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

Try: Reimplement Block Supports #26111

Closed
wants to merge 18 commits into from
Closed

Conversation

mcsf
Copy link
Contributor

@mcsf mcsf commented Oct 14, 2020

Feel free to skip to § Description if you are familiar with the problem.
Superseded by #26192.

Background

From the moment that Block Supports started to parse block content as HTML and manipulate it, there have been issues around encoding and escaping (e.g. missing UTF-8 conversion, lossy HTML entities, double-encoding) and around the HTML handling proper (error handling, DOM traversal, PHP API inconsistencies).

The issues themselves have been fixed, and with enough testing we expect the parsing of HTML to become sufficiently stable.

That said, it has become clear that we need to take a wider look at how Block Supports works on the server, not just as a piece that transforms HTML, but as a sustainable API for core and third-party blocks, and as a part of a larger pipeline that enhances saved blocks alongside Global Styles. This pipeline is critical to the rendering of pages in WordPress.

Goals

What Block Supports is: A means to absorb the complexities of common block tools — e.g. support for alignment, colours controls — such that a block just needs to state “I support alignment” for Gutenberg to provide editing controls and front-end results. It means that core and third-party blocks alike don’t need to individually implement all the pieces that go into offering custom alignment, custom colour or custom typography settings.

What Block Supports isn’t: a system that goes beyond that. It should take a block along with its alignment / colour / whatever attributes and render accordingly. It shouldn’t be capable of executing business logic, reconciling values, inspecting blocks more deeply, or in general have sophisticated awareness of blocks.

Keeping the latter in mind is key to meaningfully analyse current fragilities.

Fragilities

Handling HTML on the server

As hinted in § Background, PHP can deal with HTML, but it’s a delicate operation. It’s no accident that most of WordPress has traditionally handled content as strings throughout its components: the tradeoff is less power in exchange for performance and safety. Thus, our utmost diligence is required when we set out to introduce a system that processes a new HTML document on every render_block.

Handling blocks on the server

Building from the above, we also need to acknowledge the limitations of how much the server can understand blocks. A recent example:

#24300 sought to make Block Supports honour attribute defaults according to a block’s block.json schema. This is reasonable for simple attributes, but breaks as soon as we consider sourced attributes — which the server can’t handle — in which case the server has no idea whether to fetch the default value. The way forward is unclear.

(Despite advances brought forward by the block.json interface, the server’s inability to parse sourced attributes remains a long-standing issue. The closest we got was this experiment of Andrew’s — and even then the solution requires more HTML parsing.)

However, even if we had the necessary server-side support, the issue runs deeper: everything dealing with block validation, deprecation and migration is implemented solely on the client. And it should likely remain so: the idea has all along been that the client is meant to perform the heavy lifting of understanding blocks and making decisions based on that understanding; it can then produce the best output to be saved and rendered across environments (front end/editor, Web/Mobile, etc.).

Indirection and excessive looping

Currently, the architecture of Block Supports requires looping over all known block types at init, and again looping for each block in the content and for each known supports feature at render_block, to finally compute the necessary classes and styles to inject in the block (powered by DOMDocument for parsing and serialisation).

  • How much can we optimise this critical path?
  • How can we reduce indirection, so that block authors and other devs can be aware of the several pieces involved in rendering a block, and so that they can be in control?
  • All of this runs automatically, which is convenient, but can there be too much “magic” involved?

If the server is inherently limited — in ability and resources — in what it can do with blocks, we ought to be explicit about what and how much should be left up to the server.

Meanwhile, the recent adoption of apiVersion: 2 as a signal for “light blocks” means that we could make it a requirement that all blocks wishing to declare supports also conform to apiVersion: 2 — in the process also avoiding any issues of backwards compatibility. Edit: #26106 was just opened to purse this idea.

Description

This solution rests on the following ideas:

  • Simplify server-side processing of block content as much as possible by ditching all logic adjacent to our use of DOMDocument and instead — by taking advantage of the requirement that blocks conform to apiVersion: 2 — make strong assumptions about the shape of block content and limit content manipulation to the injection of classes and inline styles in the root element.
  • Provide a function, gutenberg_block_supports_classes, that block authors can use if they have dynamic blocks and need to explicitly state where their classes should go:
$classes = gutenberg_block_supports_classes();
return "<div class=\"outer\"><div class=\"inner $classes\">...</div></div>";
  • Keep all block-supports features centralised in a central map, and generally encapsulate Block Supports in a class to reduce the surface it exposes.

To do

  • Try proper support for inline styles, to compare against the current use of wp_add_inline_styles
  • Try supporting any kind of attribute generation, somewhat similarly to useBlockProps.save
  • Port functionality for color.background
  • Port functionality for color.gradients
  • Port functionality for color.link

How has this been tested?

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@mcsf mcsf added Framework Issues related to broader framework topics, especially as it relates to javascript Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta [Type] Experimental Experimental feature or API. labels Oct 14, 2020
@github-actions
Copy link

github-actions bot commented Oct 14, 2020

Size Change: -835 B (0%)

Total Size: 1.19 MB

Filename Size Change
build/annotations/index.js 3.54 kB -1 B
build/block-directory/index.js 8.6 kB +1 B
build/block-editor/index.js 130 kB +33 B (0%)
build/block-library/index.js 142 kB -806 B (0%)
build/block-library/style-rtl.css 7.71 kB +2 B (0%)
build/block-library/style.css 7.71 kB +2 B (0%)
build/blocks/index.js 47.6 kB +2 B (0%)
build/components/index.js 169 kB +282 B (0%)
build/components/style-rtl.css 15.4 kB -63 B (0%)
build/components/style.css 15.4 kB -61 B (0%)
build/data/index.js 8.63 kB +2 B (0%)
build/edit-navigation/index.js 10.6 kB -4 B (0%)
build/edit-post/index.js 306 kB +3 B (0%)
build/edit-site/index.js 21.1 kB -194 B (0%)
build/edit-site/style-rtl.css 3.77 kB -91 B (2%)
build/edit-site/style.css 3.77 kB -92 B (2%)
build/edit-widgets/index.js 21.4 kB +152 B (0%)
build/format-library/index.js 7.49 kB +1 B
build/reusable-blocks/index.js 3.04 kB -2 B (0%)
build/rich-text/index.js 13 kB -1 B
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/api-fetch/index.js 3.35 kB 0 B
build/autop/index.js 2.72 kB 0 B
build/blob/index.js 668 B 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-editor/style-rtl.css 10.9 kB 0 B
build/block-editor/style.css 10.9 kB 0 B
build/block-library/editor-rtl.css 8.65 kB 0 B
build/block-library/editor.css 8.65 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-default-parser/index.js 1.78 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/compose/index.js 9.63 kB 0 B
build/core-data/index.js 12.1 kB 0 B
build/data-controls/index.js 684 B 0 B
build/date/index.js 31.9 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 569 B 0 B
build/dom/index.js 4.43 kB 0 B
build/edit-navigation/style-rtl.css 868 B 0 B
build/edit-navigation/style.css 871 B 0 B
build/edit-post/style-rtl.css 6.3 kB 0 B
build/edit-post/style.css 6.29 kB 0 B
build/edit-widgets/style-rtl.css 2.97 kB 0 B
build/edit-widgets/style.css 2.97 kB 0 B
build/editor/editor-styles-rtl.css 480 B 0 B
build/editor/editor-styles.css 482 B 0 B
build/editor/index.js 42.5 kB 0 B
build/editor/style-rtl.css 3.85 kB 0 B
build/editor/style.css 3.84 kB 0 B
build/element/index.js 4.45 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 1.74 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.54 kB 0 B
build/is-shallow-equal/index.js 709 B 0 B
build/keyboard-shortcuts/index.js 2.39 kB 0 B
build/keycodes/index.js 1.85 kB 0 B
build/list-reusable-blocks/index.js 3.02 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.12 kB 0 B
build/notices/index.js 1.69 kB 0 B
build/nux/index.js 3.27 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/plugins/index.js 2.44 kB 0 B
build/primitives/index.js 1.34 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/server-side-render/index.js 2.6 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.24 kB 0 B
build/url/index.js 4.07 kB 0 B
build/viewport/index.js 1.75 kB 0 B
build/warning/index.js 1.13 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@mcsf mcsf marked this pull request as ready for review October 15, 2020 21:55
/**
* Class encapsulating and implementing Block Supports.
*/
class WP_Block_Supports {
Copy link
Contributor

Choose a reason for hiding this comment

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

This will become a public API, we should probably make it private.


$classes = '';
if ( $has_line_height ) {
$generated_class_name = uniqid( 'wp-block-lineheight-' );
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems with this PR you're also trying to change how we generate the custom styles in the server. While this is probably a sane approach, IMO, we should stick to the current way for the moment and do this in its own PR. Basically just keep the inline styles for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

which also mean wp-block-supports style shouldn't be needed yet.

Copy link
Contributor Author

@mcsf mcsf Oct 16, 2020

Choose a reason for hiding this comment

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

Agreed. That’s a remnant from when only class was supported.

Copy link
Member

Choose a reason for hiding this comment

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

As an aside if we try to generate inline style on the server. I think generating a class name per block instance and then add the styles of that block to that class name is not ideal.

I think we would have the opportunity to use atomic CSS ( each class/selector contains just one rule). Per CSS style rule we e.g: "line-height: 5px" we would have a class e.g: ".Ab { line-height: 5px }". Each block that has a line-height 5px would simply refer to that class. Our preset classes are already atomic so in the case of presets instead of generating a random class, we would simply refer to a human-readable class.
Implementing this is not complex we would have a global dictonaty that maps styles to classes e.g:

array(
	'line-height' => array(
		'5px' => 'ab'
	)
);

and a counter:

$current_class_count = 3;

Each time a style has a class we would simply include that class. If no class was defined we would encode the counter in Base36 (uses A-Z and 0-9) update the mapper to map to that class and increment the counter.

Atomic CSS is deployed in production at Facebook and Twitter and seems to reduce the amount of CSS required.

/**
* Constructs the configuration for all supported block-supports features.
*/
private function load_config() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really like this giant config here, for me each one of these should be its own file. it doesn't have to be a public API (can use a private one if the class is private) to be the case.

*
* @return string String of HTML classes.
*/
function gutenberg_block_supports_classes() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should try to get this as close as possible to useBlockProps so what about something like gutenberg_get_block_props or gutenberg_get_block_wrapper_attributes, something generic like that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, should it takes extra props as arguments and merge theme like useBlockProps does.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So would it look like this?

// latest-posts/index.php
if ( isset( $attributes ... ) $class .= ' has-dates';
if ( isset( $attributes ... ) $class .= ' has-author';

return sprintf(
  '<ul %s>%s</ul>',
  gutenberg_get_block_wrapper_attributes( array( 'class' => $class ) ),
  $list_items_markup
);

I'm not opposed :)

Copy link
Contributor

Choose a reason for hiding this comment

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

yes see #26192

Copy link
Member

Choose a reason for hiding this comment

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

So essentially it would receive an array-like

gutenberg_get_block_props( array(
	'style' => 'border-color: #f00',
	'class' => 'has-border'
) );

And return an array with new props that should be added as attributes?

I guess ideally blocks would never add attributes to the block top-level node and always use our function that merges the attributes.

We may also have a function gutenberg_get_block_props_output that returns the attributes HTML definion of the attributes "style="..." class="..." given an array of attributes.

I think a path like that would make sense.

Copy link
Member

Choose a reason for hiding this comment

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

If we continue with the current approach gutenberg_block_supports_classes what is the plan for the supports that add styles? Is having a gutenberg_block_supports_styles function the next step?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we continue with the current approach gutenberg_block_supports_classes what is the plan for the supports that add styles? Is having a gutenberg_block_supports_styles function the next step?

Right, I don't think that's a good solution.

The initial focus on classes came from wanting to see if restricting Block Supports to outputting only classes could be enough, but it's clear we should open up to more attributes.

}

$output = array();
foreach ( $this->config as $feature_name => $feature_config ) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we could add some comment here specifying what this loop is doing, at first sight, it is not easy to interpret.

*
* @return string String of HTML classes.
*/
function gutenberg_block_supports_classes() {
Copy link
Member

Choose a reason for hiding this comment

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

So essentially it would receive an array-like

gutenberg_get_block_props( array(
	'style' => 'border-color: #f00',
	'class' => 'has-border'
) );

And return an array with new props that should be added as attributes?

I guess ideally blocks would never add attributes to the block top-level node and always use our function that merges the attributes.

We may also have a function gutenberg_get_block_props_output that returns the attributes HTML definion of the attributes "style="..." class="..." given an array of attributes.

I think a path like that would make sense.

@@ -18,7 +18,14 @@ function render_block_core_cover( $attributes, $content ) {
return str_replace( 'autoplay muted', 'autoplay muted playsinline', $content );
}

return $content;
// TODO: this is just a proof of concept, not a proper solution.
$class = gutenberg_block_supports_classes();
Copy link
Member

Choose a reason for hiding this comment

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

I guess we don't need this for cover, as the supports are already applied on the client because on that block we have a proper static markup (the dynamic aspect is just a hack for something very specific that should be removed).

),
),
'callback' => function( $attributes, $block_name ) {
die( 42 );
Copy link
Member

Choose a reason for hiding this comment

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

Should we remove this die call :)?


$classes = '';
if ( $has_line_height ) {
$generated_class_name = uniqid( 'wp-block-lineheight-' );
Copy link
Member

Choose a reason for hiding this comment

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

As an aside if we try to generate inline style on the server. I think generating a class name per block instance and then add the styles of that block to that class name is not ideal.

I think we would have the opportunity to use atomic CSS ( each class/selector contains just one rule). Per CSS style rule we e.g: "line-height: 5px" we would have a class e.g: ".Ab { line-height: 5px }". Each block that has a line-height 5px would simply refer to that class. Our preset classes are already atomic so in the case of presets instead of generating a random class, we would simply refer to a human-readable class.
Implementing this is not complex we would have a global dictonaty that maps styles to classes e.g:

array(
	'line-height' => array(
		'5px' => 'ab'
	)
);

and a counter:

$current_class_count = 3;

Each time a style has a class we would simply include that class. If no class was defined we would encode the counter in Base36 (uses A-Z and 0-9) update the mapper to map to that class and increment the counter.

Atomic CSS is deployed in production at Facebook and Twitter and seems to reduce the amount of CSS required.

@@ -171,6 +171,8 @@ function render_block_core_latest_posts( $attributes ) {
$class .= ' has-author';
}

$class .= ' begin-explicitly-added ' . gutenberg_block_supports_classes() . ' end-explicitly-added';
Copy link
Member

Choose a reason for hiding this comment

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

begin-explicitly-added and end-explicitly-added are test classes right? Should be removed before the merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, all changes in block-library were for testing purposes. :)

@@ -391,6 +399,8 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse
return $pre_render;
}

$current_parsed_block = $parsed_block;
Copy link
Member

Choose a reason for hiding this comment

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

We never know how this global will end up being used, a way to avoid having the global would be having a current_parsed_block private variable as part of WP_Block_Supports instance and a letter that allows setting the variable. Here instead of setting the variable, we would call that setter.

*
* @return string String of HTML classes.
*/
function gutenberg_block_supports_classes() {
Copy link
Member

Choose a reason for hiding this comment

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

If we continue with the current approach gutenberg_block_supports_classes what is the plan for the supports that add styles? Is having a gutenberg_block_supports_styles function the next step?

@@ -53,6 +53,9 @@
"supports": {
"anchor": true,
"align": true,
"color": {
Copy link
Member

Choose a reason for hiding this comment

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

The cover block does not use the supports mechanism for colors because it has some specificities (overlay) with this change the cover block has two color control UI's.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep yep, not a real change. 👌

@mcsf
Copy link
Contributor Author

mcsf commented Oct 16, 2020

Superseded by #26192.

@mcsf mcsf closed this Oct 16, 2020
@youknowriad youknowriad deleted the try/register-block-supports branch October 16, 2020 12:19
@tellthemachines tellthemachines removed the Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Oct 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Framework Issues related to broader framework topics, especially as it relates to javascript [Type] Experimental Experimental feature or API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants