wp/wp-includes/block-supports/block-style-variations.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * Block support to enable per-section styling of block types via
       
     4  * block style variations.
       
     5  *
       
     6  * @package WordPress
       
     7  * @since 6.6.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Generate block style variation instance name.
       
    12  *
       
    13  * @since 6.6.0
       
    14  * @access private
       
    15  *
       
    16  * @param array  $block     Block object.
       
    17  * @param string $variation Slug for the block style variation.
       
    18  *
       
    19  * @return string The unique variation name.
       
    20  */
       
    21 function wp_create_block_style_variation_instance_name( $block, $variation ) {
       
    22 	return $variation . '--' . md5( serialize( $block ) );
       
    23 }
       
    24 
       
    25 /**
       
    26  * Determines the block style variation names within a CSS class string.
       
    27  *
       
    28  * @since 6.6.0
       
    29  *
       
    30  * @param string $class_string CSS class string to look for a variation in.
       
    31  *
       
    32  * @return array|null The block style variation name if found.
       
    33  */
       
    34 function wp_get_block_style_variation_name_from_class( $class_string ) {
       
    35 	if ( ! is_string( $class_string ) ) {
       
    36 		return null;
       
    37 	}
       
    38 
       
    39 	preg_match_all( '/\bis-style-(?!default)(\S+)\b/', $class_string, $matches );
       
    40 	return $matches[1] ?? null;
       
    41 }
       
    42 
       
    43 /**
       
    44  * Recursively resolves any `ref` values within a block style variation's data.
       
    45  *
       
    46  * @since 6.6.0
       
    47  * @access private
       
    48  *
       
    49  * @param array $variation_data Reference to the variation data being processed.
       
    50  * @param array $theme_json     Theme.json data to retrieve referenced values from.
       
    51  */
       
    52 function wp_resolve_block_style_variation_ref_values( &$variation_data, $theme_json ) {
       
    53 	foreach ( $variation_data as $key => &$value ) {
       
    54 		// Only need to potentially process arrays.
       
    55 		if ( is_array( $value ) ) {
       
    56 			// If ref value is set, attempt to find its matching value and update it.
       
    57 			if ( array_key_exists( 'ref', $value ) ) {
       
    58 				// Clean up any invalid ref value.
       
    59 				if ( empty( $value['ref'] ) || ! is_string( $value['ref'] ) ) {
       
    60 					unset( $variation_data[ $key ] );
       
    61 				}
       
    62 
       
    63 				$value_path = explode( '.', $value['ref'] ?? '' );
       
    64 				$ref_value  = _wp_array_get( $theme_json, $value_path );
       
    65 
       
    66 				// Only update the current value if the referenced path matched a value.
       
    67 				if ( null === $ref_value ) {
       
    68 					unset( $variation_data[ $key ] );
       
    69 				} else {
       
    70 					$value = $ref_value;
       
    71 				}
       
    72 			} else {
       
    73 				// Recursively look for ref instances.
       
    74 				wp_resolve_block_style_variation_ref_values( $value, $theme_json );
       
    75 			}
       
    76 		}
       
    77 	}
       
    78 }
       
    79 /**
       
    80  * Render the block style variation's styles.
       
    81  *
       
    82  * In the case of nested blocks with variations applied, we want the parent
       
    83  * variation's styles to be rendered before their descendants. This solves the
       
    84  * issue of a block type being styled in both the parent and descendant: we want
       
    85  * the descendant style to take priority, and this is done by loading it after,
       
    86  * in the DOM order. This is why the variation stylesheet generation is in a
       
    87  * different filter.
       
    88  *
       
    89  * @since 6.6.0
       
    90  * @access private
       
    91  *
       
    92  * @param array $parsed_block The parsed block.
       
    93  *
       
    94  * @return array The parsed block with block style variation classname added.
       
    95  */
       
    96 function wp_render_block_style_variation_support_styles( $parsed_block ) {
       
    97 	$classes    = $parsed_block['attrs']['className'] ?? null;
       
    98 	$variations = wp_get_block_style_variation_name_from_class( $classes );
       
    99 
       
   100 	if ( ! $variations ) {
       
   101 		return $parsed_block;
       
   102 	}
       
   103 
       
   104 	$tree       = WP_Theme_JSON_Resolver::get_merged_data();
       
   105 	$theme_json = $tree->get_raw_data();
       
   106 
       
   107 	// Only the first block style variation with data is supported.
       
   108 	$variation_data = array();
       
   109 	foreach ( $variations as $variation ) {
       
   110 		$variation_data = $theme_json['styles']['blocks'][ $parsed_block['blockName'] ]['variations'][ $variation ] ?? array();
       
   111 
       
   112 		if ( ! empty( $variation_data ) ) {
       
   113 			break;
       
   114 		}
       
   115 	}
       
   116 
       
   117 	if ( empty( $variation_data ) ) {
       
   118 		return $parsed_block;
       
   119 	}
       
   120 
       
   121 	/*
       
   122 	 * Recursively resolve any ref values with the appropriate value within the
       
   123 	 * theme_json data.
       
   124 	 */
       
   125 	wp_resolve_block_style_variation_ref_values( $variation_data, $theme_json );
       
   126 
       
   127 	$variation_instance = wp_create_block_style_variation_instance_name( $parsed_block, $variation );
       
   128 	$class_name         = "is-style-$variation_instance";
       
   129 	$updated_class_name = $parsed_block['attrs']['className'] . " $class_name";
       
   130 
       
   131 	/*
       
   132 	 * Even though block style variations are effectively theme.json partials,
       
   133 	 * they can't be processed completely as though they are.
       
   134 	 *
       
   135 	 * Block styles support custom selectors to direct specific types of styles
       
   136 	 * to inner elements. For example, borders on Image block's get applied to
       
   137 	 * the inner `img` element rather than the wrapping `figure`.
       
   138 	 *
       
   139 	 * The following relocates the "root" block style variation styles to
       
   140 	 * under an appropriate blocks property to leverage the preexisting style
       
   141 	 * generation for simple block style variations. This way they get the
       
   142 	 * custom selectors they need.
       
   143 	 *
       
   144 	 * The inner elements and block styles for the variation itself are
       
   145 	 * still included at the top level but scoped by the variation's selector
       
   146 	 * when the stylesheet is generated.
       
   147 	 */
       
   148 	$elements_data = $variation_data['elements'] ?? array();
       
   149 	$blocks_data   = $variation_data['blocks'] ?? array();
       
   150 	unset( $variation_data['elements'] );
       
   151 	unset( $variation_data['blocks'] );
       
   152 
       
   153 	_wp_array_set(
       
   154 		$blocks_data,
       
   155 		array( $parsed_block['blockName'], 'variations', $variation_instance ),
       
   156 		$variation_data
       
   157 	);
       
   158 
       
   159 	$config = array(
       
   160 		'version' => WP_Theme_JSON::LATEST_SCHEMA,
       
   161 		'styles'  => array(
       
   162 			'elements' => $elements_data,
       
   163 			'blocks'   => $blocks_data,
       
   164 		),
       
   165 	);
       
   166 
       
   167 	// Turn off filter that excludes block nodes. They are needed here for the variation's inner block types.
       
   168 	if ( ! is_admin() ) {
       
   169 		remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
       
   170 	}
       
   171 
       
   172 	// Temporarily prevent variation instance from being sanitized while processing theme.json.
       
   173 	$styles_registry = WP_Block_Styles_Registry::get_instance();
       
   174 	$styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) );
       
   175 
       
   176 	$variation_theme_json = new WP_Theme_JSON( $config, 'blocks' );
       
   177 	$variation_styles     = $variation_theme_json->get_stylesheet(
       
   178 		array( 'styles' ),
       
   179 		array( 'custom' ),
       
   180 		array(
       
   181 			'include_block_style_variations' => true,
       
   182 			'skip_root_layout_styles'        => true,
       
   183 			'scope'                          => ".$class_name",
       
   184 		)
       
   185 	);
       
   186 
       
   187 	// Clean up temporary block style now instance styles have been processed.
       
   188 	$styles_registry->unregister( $parsed_block['blockName'], $variation_instance );
       
   189 
       
   190 	// Restore filter that excludes block nodes.
       
   191 	if ( ! is_admin() ) {
       
   192 		add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
       
   193 	}
       
   194 
       
   195 	if ( empty( $variation_styles ) ) {
       
   196 		return $parsed_block;
       
   197 	}
       
   198 
       
   199 	wp_register_style( 'block-style-variation-styles', false, array( 'wp-block-library', 'global-styles' ) );
       
   200 	wp_add_inline_style( 'block-style-variation-styles', $variation_styles );
       
   201 
       
   202 	/*
       
   203 	 * Add variation instance class name to block's className string so it can
       
   204 	 * be enforced in the block markup via render_block filter.
       
   205 	 */
       
   206 	_wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name );
       
   207 
       
   208 	return $parsed_block;
       
   209 }
       
   210 
       
   211 /**
       
   212  * Ensure the variation block support class name generated and added to
       
   213  * block attributes in the `render_block_data` filter gets applied to the
       
   214  * block's markup.
       
   215  *
       
   216  * @see wp_render_block_style_variation_support_styles
       
   217  *
       
   218  * @since 6.6.0
       
   219  * @access private
       
   220  *
       
   221  * @param  string $block_content Rendered block content.
       
   222  * @param  array  $block         Block object.
       
   223  *
       
   224  * @return string                Filtered block content.
       
   225  */
       
   226 function wp_render_block_style_variation_class_name( $block_content, $block ) {
       
   227 	if ( ! $block_content || empty( $block['attrs']['className'] ) ) {
       
   228 		return $block_content;
       
   229 	}
       
   230 
       
   231 	/*
       
   232 	 * Matches a class prefixed by `is-style`, followed by the
       
   233 	 * variation slug, then `--`, and finally a hash.
       
   234 	 *
       
   235 	 * See `wp_create_block_style_variation_instance_name` for class generation.
       
   236 	 */
       
   237 	preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches );
       
   238 
       
   239 	if ( empty( $matches ) ) {
       
   240 		return $block_content;
       
   241 	}
       
   242 
       
   243 	$tags = new WP_HTML_Tag_Processor( $block_content );
       
   244 
       
   245 	if ( $tags->next_tag() ) {
       
   246 		/*
       
   247 		 * Ensure the variation instance class name set in the
       
   248 		 * `render_block_data` filter is applied in markup.
       
   249 		 * See `wp_render_block_style_variation_support_styles`.
       
   250 		 */
       
   251 		$tags->add_class( $matches[0] );
       
   252 	}
       
   253 
       
   254 	return $tags->get_updated_html();
       
   255 }
       
   256 
       
   257 /**
       
   258  * Enqueues styles for block style variations.
       
   259  *
       
   260  * @since 6.6.0
       
   261  * @access private
       
   262  */
       
   263 function wp_enqueue_block_style_variation_styles() {
       
   264 	wp_enqueue_style( 'block-style-variation-styles' );
       
   265 }
       
   266 
       
   267 // Register the block support.
       
   268 WP_Block_Supports::get_instance()->register( 'block-style-variation', array() );
       
   269 
       
   270 add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 );
       
   271 add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 );
       
   272 add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 );
       
   273 
       
   274 /**
       
   275  * Registers block style variations read in from theme.json partials.
       
   276  *
       
   277  * @since 6.6.0
       
   278  * @access private
       
   279  *
       
   280  * @param array $variations Shared block style variations.
       
   281  */
       
   282 function wp_register_block_style_variations_from_theme_json_partials( $variations ) {
       
   283 	if ( empty( $variations ) ) {
       
   284 		return;
       
   285 	}
       
   286 
       
   287 	$registry = WP_Block_Styles_Registry::get_instance();
       
   288 
       
   289 	foreach ( $variations as $variation ) {
       
   290 		if ( empty( $variation['blockTypes'] ) || empty( $variation['styles'] ) ) {
       
   291 			continue;
       
   292 		}
       
   293 
       
   294 		$variation_name  = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] );
       
   295 		$variation_label = $variation['title'] ?? $variation_name;
       
   296 
       
   297 		foreach ( $variation['blockTypes'] as $block_type ) {
       
   298 			$registered_styles = $registry->get_registered_styles_for_block( $block_type );
       
   299 
       
   300 			// Register block style variation if it hasn't already been registered.
       
   301 			if ( ! array_key_exists( $variation_name, $registered_styles ) ) {
       
   302 				register_block_style(
       
   303 					$block_type,
       
   304 					array(
       
   305 						'name'  => $variation_name,
       
   306 						'label' => $variation_label,
       
   307 					)
       
   308 				);
       
   309 			}
       
   310 		}
       
   311 	}
       
   312 }