WordPress/wp-includes/block-supports/block-style-variations.php

425 lines
14 KiB
PHP

<?php
/**
* Block support to enable per-section styling of block types via
* block style variations.
*
* @package WordPress
* @since 6.6.0
*/
/**
* Generate block style variation instance name.
*
* @since 6.6.0
* @access private
*
* @param array $block Block object.
* @param string $variation Slug for the block style variation.
*
* @return string The unique variation name.
*/
function wp_create_block_style_variation_instance_name( $block, $variation ) {
return $variation . '--' . md5( serialize( $block ) );
}
/**
* Determines the block style variation names within a CSS class string.
*
* @since 6.6.0
*
* @param string $class_string CSS class string to look for a variation in.
*
* @return array|null The block style variation name if found.
*/
function wp_get_block_style_variation_name_from_class( $class_string ) {
if ( ! is_string( $class_string ) ) {
return null;
}
preg_match_all( '/\bis-style-(?!default)(\S+)\b/', $class_string, $matches );
return $matches[1] ?? null;
}
/**
* Render the block style variation's styles.
*
* In the case of nested blocks with variations applied, we want the parent
* variation's styles to be rendered before their descendants. This solves the
* issue of a block type being styled in both the parent and descendant: we want
* the descendant style to take priority, and this is done by loading it after,
* in the DOM order. This is why the variation stylesheet generation is in a
* different filter.
*
* @since 6.6.0
* @access private
*
* @param array $parsed_block The parsed block.
*
* @return array The parsed block with block style variation classname added.
*/
function wp_render_block_style_variation_support_styles( $parsed_block ) {
$classes = $parsed_block['attrs']['className'] ?? null;
$variations = wp_get_block_style_variation_name_from_class( $classes );
if ( ! $variations ) {
return $parsed_block;
}
$tree = WP_Theme_JSON_Resolver::get_merged_data();
$theme_json = $tree->get_raw_data();
// Only the first block style variation with data is supported.
$variation_data = array();
foreach ( $variations as $variation ) {
$variation_data = $theme_json['styles']['blocks'][ $parsed_block['blockName'] ]['variations'][ $variation ] ?? array();
if ( ! empty( $variation_data ) ) {
break;
}
}
if ( empty( $variation_data ) ) {
return $parsed_block;
}
$variation_instance = wp_create_block_style_variation_instance_name( $parsed_block, $variation );
$class_name = "is-style-$variation_instance";
$updated_class_name = $parsed_block['attrs']['className'] . " $class_name";
/*
* Even though block style variations are effectively theme.json partials,
* they can't be processed completely as though they are.
*
* Block styles support custom selectors to direct specific types of styles
* to inner elements. For example, borders on Image block's get applied to
* the inner `img` element rather than the wrapping `figure`.
*
* The following relocates the "root" block style variation styles to
* under an appropriate blocks property to leverage the preexisting style
* generation for simple block style variations. This way they get the
* custom selectors they need.
*
* The inner elements and block styles for the variation itself are
* still included at the top level but scoped by the variation's selector
* when the stylesheet is generated.
*/
$elements_data = $variation_data['elements'] ?? array();
$blocks_data = $variation_data['blocks'] ?? array();
unset( $variation_data['elements'] );
unset( $variation_data['blocks'] );
_wp_array_set(
$blocks_data,
array( $parsed_block['blockName'], 'variations', $variation_instance ),
$variation_data
);
$config = array(
'version' => WP_Theme_JSON::LATEST_SCHEMA,
'styles' => array(
'elements' => $elements_data,
'blocks' => $blocks_data,
),
);
// Turn off filter that excludes block nodes. They are needed here for the variation's inner block types.
if ( ! is_admin() ) {
remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
}
// Temporarily prevent variation instance from being sanitized while processing theme.json.
$styles_registry = WP_Block_Styles_Registry::get_instance();
$styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) );
$variation_theme_json = new WP_Theme_JSON( $config, 'blocks' );
$variation_styles = $variation_theme_json->get_stylesheet(
array( 'styles' ),
array( 'custom' ),
array(
'skip_root_layout_styles' => true,
'scope' => ".$class_name",
)
);
// Clean up temporary block style now instance styles have been processed.
$styles_registry->unregister( $parsed_block['blockName'], $variation_instance );
// Restore filter that excludes block nodes.
if ( ! is_admin() ) {
add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
}
if ( empty( $variation_styles ) ) {
return $parsed_block;
}
wp_register_style( 'block-style-variation-styles', false, array( 'global-styles', 'wp-block-library' ) );
wp_add_inline_style( 'block-style-variation-styles', $variation_styles );
/*
* Add variation instance class name to block's className string so it can
* be enforced in the block markup via render_block filter.
*/
_wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name );
return $parsed_block;
}
/**
* Ensure the variation block support class name generated and added to
* block attributes in the `render_block_data` filter gets applied to the
* block's markup.
*
* @see wp_render_block_style_variation_support_styles
*
* @since 6.6.0
* @access private
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
*
* @return string Filtered block content.
*/
function wp_render_block_style_variation_class_name( $block_content, $block ) {
if ( ! $block_content || empty( $block['attrs']['className'] ) ) {
return $block_content;
}
/*
* Matches a class prefixed by `is-style`, followed by the
* variation slug, then `--`, and finally a hash.
*
* See `wp_create_block_style_variation_instance_name` for class generation.
*/
preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches );
if ( empty( $matches ) ) {
return $block_content;
}
$tags = new WP_HTML_Tag_Processor( $block_content );
if ( $tags->next_tag() ) {
/*
* Ensure the variation instance class name set in the
* `render_block_data` filter is applied in markup.
* See `wp_render_block_style_variation_support_styles`.
*/
$tags->add_class( $matches[0] );
}
return $tags->get_updated_html();
}
/**
* Collects block style variation data for merging with theme.json data.
* As each block style variation is processed it is registered if it hasn't
* been already. This registration is required for later sanitization of
* theme.json data.
*
* @since 6.6.0
* @access private
*
* @param array $variations Shared block style variations.
*
* @return array Block variations data to be merged under `styles.blocks`.
*/
function wp_resolve_and_register_block_style_variations( $variations ) {
$variations_data = array();
if ( empty( $variations ) ) {
return $variations_data;
}
$registry = WP_Block_Styles_Registry::get_instance();
$have_named_variations = ! wp_is_numeric_array( $variations );
foreach ( $variations as $key => $variation ) {
$supported_blocks = $variation['blockTypes'] ?? array();
/*
* Standalone theme.json partial files for block style variations
* will have their styles under a top-level property by the same name.
* Variations defined within an existing theme.json or theme style
* variation will themselves already be the required styles data.
*/
$variation_data = $variation['styles'] ?? $variation;
if ( empty( $variation_data ) ) {
continue;
}
/*
* Block style variations read in via standalone theme.json partials
* need to have their name set to the kebab case version of their title.
*/
$variation_name = $have_named_variations ? $key : _wp_to_kebab_case( $variation['title'] );
$variation_label = $variation['title'] ?? $variation_name;
foreach ( $supported_blocks as $block_type ) {
$registered_styles = $registry->get_registered_styles_for_block( $block_type );
// Register block style variation if it hasn't already been registered.
if ( ! array_key_exists( $variation_name, $registered_styles ) ) {
register_block_style(
$block_type,
array(
'name' => $variation_name,
'label' => $variation_label,
)
);
}
// Add block style variation data under current block type.
$path = array( $block_type, 'variations', $variation_name );
_wp_array_set( $variations_data, $path, $variation_data );
}
}
return $variations_data;
}
/**
* Merges variations data with existing theme.json data ensuring that the
* current theme.json data values take precedence.
*
* @since 6.6.0
* @access private
*
* @param array $variations_data Block style variations data keyed by block type.
* @param WP_Theme_JSON_Data $theme_json Current theme.json data.
* @param string $origin Origin for the theme.json data.
*
* @return WP_Theme_JSON The merged theme.json data.
*/
function wp_merge_block_style_variations_data( $variations_data, $theme_json, $origin = 'theme' ) {
if ( empty( $variations_data ) ) {
return $theme_json;
}
$variations_theme_json_data = array(
'version' => WP_Theme_JSON::LATEST_SCHEMA,
'styles' => array( 'blocks' => $variations_data ),
);
$variations_theme_json = new WP_Theme_JSON_Data( $variations_theme_json_data, $origin );
/*
* Merge the current theme.json data over shared variation data so that
* any explicit per block variation values take precedence.
*/
return $variations_theme_json->update_with( $theme_json->get_data() );
}
/**
* Merges any shared block style variation definitions from a theme style
* variation into their appropriate block type within theme json styles. Any
* custom user selections already made will take precedence over the shared
* style variation value.
*
* @since 6.6.0
* @access private
*
* @param WP_Theme_JSON_Data $theme_json Current theme.json data.
*
* @return WP_Theme_JSON_Data
*/
function wp_resolve_block_style_variations_from_theme_style_variation( $theme_json ) {
$theme_json_data = $theme_json->get_data();
$shared_variations = $theme_json_data['styles']['blocks']['variations'] ?? array();
$variations_data = wp_resolve_and_register_block_style_variations( $shared_variations );
return wp_merge_block_style_variations_data( $variations_data, $theme_json, 'user' );
}
/**
* Merges block style variation data sourced from standalone partial
* theme.json files.
*
* @since 6.6.0
* @access private
*
* @param WP_Theme_JSON_Data $theme_json Current theme.json data.
*
* @return WP_Theme_JSON_Data
*/
function wp_resolve_block_style_variations_from_theme_json_partials( $theme_json ) {
$block_style_variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
$variations_data = wp_resolve_and_register_block_style_variations( $block_style_variations );
return wp_merge_block_style_variations_data( $variations_data, $theme_json );
}
/**
* Merges shared block style variations registered within the
* `styles.blocks.variations` property of the primary theme.json file.
*
* @since 6.6.0
* @access private
*
* @param WP_Theme_JSON_Data $theme_json Current theme.json data.
*
* @return WP_Theme_JSON_Data
*/
function wp_resolve_block_style_variations_from_primary_theme_json( $theme_json ) {
$theme_json_data = $theme_json->get_data();
$block_style_variations = $theme_json_data['styles']['blocks']['variations'] ?? array();
$variations_data = wp_resolve_and_register_block_style_variations( $block_style_variations );
return wp_merge_block_style_variations_data( $variations_data, $theme_json );
}
/**
* Merges block style variations registered via the block styles registry with a
* style object, under their appropriate block types within theme.json styles.
* Any variation values defined within the theme.json specific to a block type
* will take precedence over these shared definitions.
*
* @since 6.6.0
* @access private
*
* @param WP_Theme_JSON_Data $theme_json Current theme.json data.
*
* @return WP_Theme_JSON_Data
*/
function wp_resolve_block_style_variations_from_styles_registry( $theme_json ) {
$registry = WP_Block_Styles_Registry::get_instance();
$styles = $registry->get_all_registered();
$variations_data = array();
foreach ( $styles as $block_type => $variations ) {
foreach ( $variations as $variation_name => $variation ) {
if ( ! empty( $variation['style_data'] ) ) {
$path = array( $block_type, 'variations', $variation_name );
_wp_array_set( $variations_data, $path, $variation['style_data'] );
}
}
}
return wp_merge_block_style_variations_data( $variations_data, $theme_json );
}
/**
* Enqueues styles for block style variations.
*
* @since 6.6.0
* @access private
*/
function wp_enqueue_block_style_variation_styles() {
wp_enqueue_style( 'block-style-variation-styles' );
}
// Register the block support.
WP_Block_Supports::get_instance()->register( 'block-style-variation', array() );
add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 );
add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 );
add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 );
// Resolve block style variations from all their potential sources. The order here is deliberate.
add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_primary_theme_json', 10, 1 );
add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_theme_json_partials', 10, 1 );
add_filter( 'wp_theme_json_data_theme', 'wp_resolve_block_style_variations_from_styles_registry', 10, 1 );
add_filter( 'wp_theme_json_data_user', 'wp_resolve_block_style_variations_from_theme_style_variation', 10, 1 );