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

Add: Frontend section presets output #42124

Merged
merged 2 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
151 changes: 151 additions & 0 deletions lib/block-supports/settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* Block level presets support.
*
* @package gutenberg
*/

/**
* Get the class name used on block level presets.
*
* @access private
*
* @param array $block Block object.
* @return string The unique class name.
*/
function _gutenberg_get_presets_class_name( $block ) {
oandregal marked this conversation as resolved.
Show resolved Hide resolved
return 'wp-settings-' . md5( serialize( $block ) );
}

/**
* Update the block content with block level presets class name.
*
* @access private
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
*/
function _gutenberg_add_block_level_presets_class( $block_content, $block ) {
if ( ! $block_content ) {
return $block_content;
}

// return early if the block doesn't have support for settings.
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
if ( ! block_has_support( $block_type, array( '__experimentalSettings' ), false ) ) {
return $block_content;
}

// return early if no settings are found on the block attributes.
$block_settings = _wp_array_get( $block, array( 'attrs', 'settings' ), null );
if ( empty( $block_settings ) ) {
return $block_content;
}

$class_name = _gutenberg_get_presets_class_name( $block );

// Like the layout hook this assumes the hook only applies to blocks with a single wrapper.
// Retrieve the opening tag of the first HTML element.
$html_element_matches = array();
preg_match( '/<[^>]+>/', $block_content, $html_element_matches, PREG_OFFSET_CAPTURE );
$first_element = $html_element_matches[0][0];
// If the first HTML element has a class attribute just add the new class
// as we do on layout and duotone.
if ( strpos( $first_element, 'class="' ) !== false ) {
$content = preg_replace(
'/' . preg_quote( 'class="', '/' ) . '/',
'class="' . $class_name . ' ',
$block_content,
1
);
} else {
// If the first HTML element has no class attribute we should inject the attribute before the attribute at the end.
$first_element_offset = $html_element_matches[0][1];
$content = substr_replace( $block_content, ' class="' . $class_name . '"', $first_element_offset + strlen( $first_element ) - 1, 0 );
}

return $content;
}

/**
* Render the block level presets stylesheet.
*
* @access private
*
* @param string|null $pre_render The pre-rendered content. Default null.
* @param array $block The block being rendered.
*
* @return null
*/
function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) {
// Return early if the block has not support for descendent block styles.
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
if ( ! block_has_support( $block_type, array( '__experimentalSettings' ), false ) ) {
return null;
}

// return early if no settings are found on the block attributes.
$block_settings = _wp_array_get( $block, array( 'attrs', 'settings' ), null );
if ( empty( $block_settings ) ) {
return null;
}

$class_name = '.' . _gutenberg_get_presets_class_name( $block );

// the root selector for preset variables needs to target every possible block selector
// in order for the general setting to override any bock specific setting of a parent block or
// the site root.
$variables_root_selector = ',[class*="wp-block"]';
$registry = WP_Block_Type_Registry::get_instance();
$blocks = $registry->get_all_registered();
foreach ( $blocks as $block_type ) {
if (
isset( $block_type->supports['__experimentalSelector'] ) &&
is_string( $block_type->supports['__experimentalSelector'] )
) {
$variables_root_selector .= ',' . $block_type->supports['__experimentalSelector'];
}
}
$variables_root_selector = WP_Theme_JSON_6_1::scope_selector( $variables_root_selector, $class_name );

// Remove any potentially unsafe styles.
$theme_json_shape = WP_Theme_JSON_Gutenberg::remove_insecure_properties(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'settings' => $block_settings,
)
);
$theme_json_object = new WP_Theme_JSON_Gutenberg( $theme_json_shape );
Copy link
Member

Choose a reason for hiding this comment

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

🤔 We don't have a version for the settings stored in the block, as we do in the theme.json. This is problematic because we assume that the settings stored in the block use the latest format, se we can run into this issue:

  • a post has a container with settings formatted as of version 2
  • we introduce a theme.json version 3 with breaking changes
  • because the settings in the block don't have a version it'll assume it's the latest (version 3), and we don't know what happens (depends on the changes)

Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking that we may need to store the version in the block settings as well, unless we make the migration code to infer the version from the data. Thoughts?

If we add this field, how should we name it? Using themeJsonVersion sounds a bit weird, although I don't have anything against. settingsVersion could work but if we add later styles is a bit limiting. All things considered, themeJsonVersion could work.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we can add a settings version attribute. What would be your preferred name: settingsVersion, themeJsonVersion (implying it could also end up used in styles), version inside settings field (it would make setting object in blocks diverge from settings in theme.json), or another name? Just "version" as top-level attribute does not look good as it seems to apply to everything, not just theme.json-related stuff.

Copy link
Member

Choose a reason for hiding this comment

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

This is an issue that precedes this PR. We can address it in a follow-up. We need to make sure there's always a version in the block attributes and both client & server discard the settings if there's not.


$styles = '';

// include preset css variables declaration on the stylesheet.
$styles .= $theme_json_object->get_stylesheet(
array( 'variables' ),
null,
array(
'root_selector' => $variables_root_selector . ',' . $class_name . ' *',
'scope' => $class_name,
)
);

// include preset css classes on the the stylesheet.
$styles .= $theme_json_object->get_stylesheet(
array( 'presets' ),
null,
array(
'root_selector' => $class_name . ',' . $class_name . ' *',
'scope' => $class_name,
)
);

if ( ! empty( $styles ) ) {
gutenberg_enqueue_block_support_styles( $styles );
}

return null;
}

add_filter( 'render_block', '_gutenberg_add_block_level_presets_class', 10, 2 );
add_filter( 'pre_render_block', '_gutenberg_add_block_level_preset_styles', 10, 2 );
93 changes: 85 additions & 8 deletions lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ protected static function get_blocks_metadata() {
static::$blocks_metadata[ $block_name ]['features'] = $features;
}

// Assign defaults, then overwrite those that the block sets by itself.
// Assign defaults, then override those that the block sets by itself.
// If the block selector is compounded, will append the element to each
// individual block selector.
$block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] );
Expand Down Expand Up @@ -625,9 +625,12 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) {
* 'styles': only the styles section in theme.json.
* 'presets': only the classes for the presets.
* @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
* @param array $options An array of options for now used for internal purposes only (may change without notice).
* The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
* and root_selector which overwrites and forces a given selector to be used on the root node.
* @return string Stylesheet.
*/
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
}
Expand All @@ -648,30 +651,58 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets'
$style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

$root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
Copy link
Member

Choose a reason for hiding this comment

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

Nice one ❤️ Wasn't super comfortable hard-coding the 0 index.

$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );

if ( ! empty( $options['scope'] ) ) {
foreach ( $setting_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
foreach ( $style_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
}

if ( ! empty( $options['root_selector'] ) ) {
if ( false !== $root_settings_key ) {
$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
}
if ( false !== $root_style_key ) {
$setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
}
}

$stylesheet = '';

if ( in_array( 'variables', $types, true ) ) {
$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
}

if ( in_array( 'styles', $types, true ) ) {
$root_block_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );

if ( false !== $root_block_key ) {
$stylesheet .= $this->get_root_layout_rules( static::ROOT_BLOCK_SELECTOR, $style_nodes[ $root_block_key ] );
if ( false !== $root_style_key ) {
$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
}
$stylesheet .= $this->get_block_classes( $style_nodes );
} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
$root_selector = static::ROOT_BLOCK_SELECTOR;
Copy link
Member

@oandregal oandregal Sep 22, 2022

Choose a reason for hiding this comment

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

This gist of code is not used at the moment, correct? My understanding is that it is just a way to make the scope and root_selector options work with this part of the code as well. I wasn't sure how to test this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it is not used at the moment, but it is to make sure the function also works with base-layout-styles origin. I don't think it will be needed but I preferred to make the function consistent.

$columns_selector = '.wp-block-columns';
if ( ! empty( $options['scope'] ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be?

Suggested change
if ( ! empty( $options['scope'] ) ) {
if ( ! empty( $options['root_selector'] ) ) {

$root_selector = $options['root_selector'];
}
if ( ! empty( $options['scope'] ) ) {
$root_selector = static::scope_selector( $options['scope'], $root_selector );
$columns_selector = static::scope_selector( $options['scope'], $columns_selector );
}
// Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
// For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
$base_styles_nodes = array(
array(
'path' => array( 'styles' ),
'selector' => static::ROOT_BLOCK_SELECTOR,
'selector' => $root_selector,
),
array(
'path' => array( 'styles', 'blocks', 'core/columns' ),
'selector' => '.wp-block-columns',
'selector' => $columns_selector,
'name' => 'core/columns',
),
);
Expand Down Expand Up @@ -1516,4 +1547,50 @@ protected function get_layout_styles( $block_metadata ) {
}
return $block_rules;
}

/**
* Function that scopes a selector with another one. This works a bit like
* SCSS nesting except the `&` operator isn't supported.
*
* <code>
* $scope = '.a, .b .c';
* $selector = '> .x, .y';
* $merged = scope_selector( $scope, $selector );
* // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
* </code>
*
* @since 5.9.0
Copy link
Member

Choose a reason for hiding this comment

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

Is it removing this and add a comment with what changed? I understand the change is making sure no empty scope ends up being used, but not sure why we didn't that before.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think the changes we are making to the function are worth a comment. We are just fixing some cases with empty selectors.

*
* @param string $scope Selector to scope to.
* @param string $selector Original selector.
* @return string Scoped selector.
*/
public static function scope_selector( $scope, $selector ) {
$scopes = explode( ',', $scope );
$selectors = explode( ',', $selector );

$selectors_scoped = array();
foreach ( $scopes as $outer ) {
foreach ( $selectors as $inner ) {
$outer = trim( $outer );
$inner = trim( $inner );
if ( empty( $outer ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

This is purely a style choice, it's totally up to you. Personally, I find this more readable:

if ( empty( $outer ) && empty( $inner) ) { }
else if ( empty( $outer ) ) {}
else if ( empty( $inner ) ) {}
else {}

if ( empty( $inner ) ) {
continue;
}
$selectors_scoped[] = $inner;
} else {
if ( empty( $inner ) ) {
$selectors_scoped[] = $outer;
} else {
$selectors_scoped[] = $outer . ' ' . $inner;
}
}
}
}

$result = implode( ', ', $selectors_scoped );
return $result;
}

}
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ function gutenberg_is_experiment_enabled( $name ) {

// Block supports overrides.
require __DIR__ . '/block-supports/utils.php';
require __DIR__ . '/block-supports/settings.php';
require __DIR__ . '/block-supports/elements.php';
require __DIR__ . '/block-supports/colors.php';
require __DIR__ . '/block-supports/typography.php';
Expand Down