wp/wp-includes/style-engine/class-wp-style-engine.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * Style Engine: WP_Style_Engine class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage StyleEngine
       
     7  * @since 6.1.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * The main class integrating all other WP_Style_Engine_* classes.
       
    12  *
       
    13  * The Style Engine aims to provide a consistent API for rendering styling for blocks
       
    14  * across both client-side and server-side applications.
       
    15  *
       
    16  * This class is final and should not be extended.
       
    17  *
       
    18  * This class is for internal Core usage and is not supposed to be used by extenders
       
    19  * (plugins and/or themes). This is a low-level API that may need to do breaking changes.
       
    20  * Please, use wp_style_engine_get_styles() instead.
       
    21  *
       
    22  * @access private
       
    23  * @since 6.1.0
       
    24  * @since 6.3.0 Added support for text-columns.
       
    25  * @since 6.4.0 Added support for background.backgroundImage.
       
    26  * @since 6.5.0 Added support for background.backgroundPosition,
       
    27  *              background.backgroundRepeat and dimensions.aspectRatio.
       
    28  */
       
    29 #[AllowDynamicProperties]
       
    30 final class WP_Style_Engine {
       
    31 	/**
       
    32 	 * Style definitions that contain the instructions to parse/output valid Gutenberg styles from a block's attributes.
       
    33 	 *
       
    34 	 * For every style definition, the following properties are valid:
       
    35 	 *
       
    36 	 *  - classnames    => (array) an array of classnames to be returned for block styles. The key is a classname or pattern.
       
    37 	 *                    A value of `true` means the classname should be applied always. Otherwise, a valid CSS property (string)
       
    38 	 *                    to match the incoming value, e.g., "color" to match var:preset|color|somePresetSlug.
       
    39 	 *  - css_vars      => (array) an array of key value pairs used to generate CSS var values.
       
    40 	 *                     The key should be the CSS property name that matches the second element of the preset string value,
       
    41 	 *                     i.e., "color" in var:preset|color|somePresetSlug. The value is a CSS var pattern (e.g. `--wp--preset--color--$slug`),
       
    42 	 *                     whose `$slug` fragment will be replaced with the preset slug, which is the third element of the preset string value,
       
    43 	 *                     i.e., `somePresetSlug` in var:preset|color|somePresetSlug.
       
    44 	 *  - property_keys => (array) array of keys whose values represent a valid CSS property, e.g., "margin" or "border".
       
    45 	 *  - path          => (array) a path that accesses the corresponding style value in the block style object.
       
    46 	 *  - value_func    => (string) the name of a function to generate a CSS definition array for a particular style object. The output of this function should be `array( "$property" => "$value", ... )`.
       
    47 	 *
       
    48 	 * @since 6.1.0
       
    49 	 * @var array
       
    50 	 */
       
    51 	const BLOCK_STYLE_DEFINITIONS_METADATA = array(
       
    52 		'background' => array(
       
    53 			'backgroundImage'    => array(
       
    54 				'property_keys' => array(
       
    55 					'default' => 'background-image',
       
    56 				),
       
    57 				'value_func'    => array( self::class, 'get_url_or_value_css_declaration' ),
       
    58 				'path'          => array( 'background', 'backgroundImage' ),
       
    59 			),
       
    60 			'backgroundPosition' => array(
       
    61 				'property_keys' => array(
       
    62 					'default' => 'background-position',
       
    63 				),
       
    64 				'path'          => array( 'background', 'backgroundPosition' ),
       
    65 			),
       
    66 			'backgroundRepeat'   => array(
       
    67 				'property_keys' => array(
       
    68 					'default' => 'background-repeat',
       
    69 				),
       
    70 				'path'          => array( 'background', 'backgroundRepeat' ),
       
    71 			),
       
    72 			'backgroundSize'     => array(
       
    73 				'property_keys' => array(
       
    74 					'default' => 'background-size',
       
    75 				),
       
    76 				'path'          => array( 'background', 'backgroundSize' ),
       
    77 			),
       
    78 		),
       
    79 		'color'      => array(
       
    80 			'text'       => array(
       
    81 				'property_keys' => array(
       
    82 					'default' => 'color',
       
    83 				),
       
    84 				'path'          => array( 'color', 'text' ),
       
    85 				'css_vars'      => array(
       
    86 					'color' => '--wp--preset--color--$slug',
       
    87 				),
       
    88 				'classnames'    => array(
       
    89 					'has-text-color'  => true,
       
    90 					'has-$slug-color' => 'color',
       
    91 				),
       
    92 			),
       
    93 			'background' => array(
       
    94 				'property_keys' => array(
       
    95 					'default' => 'background-color',
       
    96 				),
       
    97 				'path'          => array( 'color', 'background' ),
       
    98 				'css_vars'      => array(
       
    99 					'color' => '--wp--preset--color--$slug',
       
   100 				),
       
   101 				'classnames'    => array(
       
   102 					'has-background'             => true,
       
   103 					'has-$slug-background-color' => 'color',
       
   104 				),
       
   105 			),
       
   106 			'gradient'   => array(
       
   107 				'property_keys' => array(
       
   108 					'default' => 'background',
       
   109 				),
       
   110 				'path'          => array( 'color', 'gradient' ),
       
   111 				'css_vars'      => array(
       
   112 					'gradient' => '--wp--preset--gradient--$slug',
       
   113 				),
       
   114 				'classnames'    => array(
       
   115 					'has-background'                => true,
       
   116 					'has-$slug-gradient-background' => 'gradient',
       
   117 				),
       
   118 			),
       
   119 		),
       
   120 		'border'     => array(
       
   121 			'color'  => array(
       
   122 				'property_keys' => array(
       
   123 					'default'    => 'border-color',
       
   124 					'individual' => 'border-%s-color',
       
   125 				),
       
   126 				'path'          => array( 'border', 'color' ),
       
   127 				'classnames'    => array(
       
   128 					'has-border-color'       => true,
       
   129 					'has-$slug-border-color' => 'color',
       
   130 				),
       
   131 			),
       
   132 			'radius' => array(
       
   133 				'property_keys' => array(
       
   134 					'default'    => 'border-radius',
       
   135 					'individual' => 'border-%s-radius',
       
   136 				),
       
   137 				'path'          => array( 'border', 'radius' ),
       
   138 			),
       
   139 			'style'  => array(
       
   140 				'property_keys' => array(
       
   141 					'default'    => 'border-style',
       
   142 					'individual' => 'border-%s-style',
       
   143 				),
       
   144 				'path'          => array( 'border', 'style' ),
       
   145 			),
       
   146 			'width'  => array(
       
   147 				'property_keys' => array(
       
   148 					'default'    => 'border-width',
       
   149 					'individual' => 'border-%s-width',
       
   150 				),
       
   151 				'path'          => array( 'border', 'width' ),
       
   152 			),
       
   153 			'top'    => array(
       
   154 				'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
       
   155 				'path'       => array( 'border', 'top' ),
       
   156 				'css_vars'   => array(
       
   157 					'color' => '--wp--preset--color--$slug',
       
   158 				),
       
   159 			),
       
   160 			'right'  => array(
       
   161 				'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
       
   162 				'path'       => array( 'border', 'right' ),
       
   163 				'css_vars'   => array(
       
   164 					'color' => '--wp--preset--color--$slug',
       
   165 				),
       
   166 			),
       
   167 			'bottom' => array(
       
   168 				'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
       
   169 				'path'       => array( 'border', 'bottom' ),
       
   170 				'css_vars'   => array(
       
   171 					'color' => '--wp--preset--color--$slug',
       
   172 				),
       
   173 			),
       
   174 			'left'   => array(
       
   175 				'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
       
   176 				'path'       => array( 'border', 'left' ),
       
   177 				'css_vars'   => array(
       
   178 					'color' => '--wp--preset--color--$slug',
       
   179 				),
       
   180 			),
       
   181 		),
       
   182 		'shadow'     => array(
       
   183 			'shadow' => array(
       
   184 				'property_keys' => array(
       
   185 					'default' => 'box-shadow',
       
   186 				),
       
   187 				'path'          => array( 'shadow' ),
       
   188 				'css_vars'      => array(
       
   189 					'shadow' => '--wp--preset--shadow--$slug',
       
   190 				),
       
   191 			),
       
   192 		),
       
   193 		'dimensions' => array(
       
   194 			'aspectRatio' => array(
       
   195 				'property_keys' => array(
       
   196 					'default' => 'aspect-ratio',
       
   197 				),
       
   198 				'path'          => array( 'dimensions', 'aspectRatio' ),
       
   199 				'classnames'    => array(
       
   200 					'has-aspect-ratio' => true,
       
   201 				),
       
   202 			),
       
   203 			'minHeight'   => array(
       
   204 				'property_keys' => array(
       
   205 					'default' => 'min-height',
       
   206 				),
       
   207 				'path'          => array( 'dimensions', 'minHeight' ),
       
   208 				'css_vars'      => array(
       
   209 					'spacing' => '--wp--preset--spacing--$slug',
       
   210 				),
       
   211 			),
       
   212 		),
       
   213 		'spacing'    => array(
       
   214 			'padding' => array(
       
   215 				'property_keys' => array(
       
   216 					'default'    => 'padding',
       
   217 					'individual' => 'padding-%s',
       
   218 				),
       
   219 				'path'          => array( 'spacing', 'padding' ),
       
   220 				'css_vars'      => array(
       
   221 					'spacing' => '--wp--preset--spacing--$slug',
       
   222 				),
       
   223 			),
       
   224 			'margin'  => array(
       
   225 				'property_keys' => array(
       
   226 					'default'    => 'margin',
       
   227 					'individual' => 'margin-%s',
       
   228 				),
       
   229 				'path'          => array( 'spacing', 'margin' ),
       
   230 				'css_vars'      => array(
       
   231 					'spacing' => '--wp--preset--spacing--$slug',
       
   232 				),
       
   233 			),
       
   234 		),
       
   235 		'typography' => array(
       
   236 			'fontSize'       => array(
       
   237 				'property_keys' => array(
       
   238 					'default' => 'font-size',
       
   239 				),
       
   240 				'path'          => array( 'typography', 'fontSize' ),
       
   241 				'css_vars'      => array(
       
   242 					'font-size' => '--wp--preset--font-size--$slug',
       
   243 				),
       
   244 				'classnames'    => array(
       
   245 					'has-$slug-font-size' => 'font-size',
       
   246 				),
       
   247 			),
       
   248 			'fontFamily'     => array(
       
   249 				'property_keys' => array(
       
   250 					'default' => 'font-family',
       
   251 				),
       
   252 				'css_vars'      => array(
       
   253 					'font-family' => '--wp--preset--font-family--$slug',
       
   254 				),
       
   255 				'path'          => array( 'typography', 'fontFamily' ),
       
   256 				'classnames'    => array(
       
   257 					'has-$slug-font-family' => 'font-family',
       
   258 				),
       
   259 			),
       
   260 			'fontStyle'      => array(
       
   261 				'property_keys' => array(
       
   262 					'default' => 'font-style',
       
   263 				),
       
   264 				'path'          => array( 'typography', 'fontStyle' ),
       
   265 			),
       
   266 			'fontWeight'     => array(
       
   267 				'property_keys' => array(
       
   268 					'default' => 'font-weight',
       
   269 				),
       
   270 				'path'          => array( 'typography', 'fontWeight' ),
       
   271 			),
       
   272 			'lineHeight'     => array(
       
   273 				'property_keys' => array(
       
   274 					'default' => 'line-height',
       
   275 				),
       
   276 				'path'          => array( 'typography', 'lineHeight' ),
       
   277 			),
       
   278 			'textColumns'    => array(
       
   279 				'property_keys' => array(
       
   280 					'default' => 'column-count',
       
   281 				),
       
   282 				'path'          => array( 'typography', 'textColumns' ),
       
   283 			),
       
   284 			'textDecoration' => array(
       
   285 				'property_keys' => array(
       
   286 					'default' => 'text-decoration',
       
   287 				),
       
   288 				'path'          => array( 'typography', 'textDecoration' ),
       
   289 			),
       
   290 			'textTransform'  => array(
       
   291 				'property_keys' => array(
       
   292 					'default' => 'text-transform',
       
   293 				),
       
   294 				'path'          => array( 'typography', 'textTransform' ),
       
   295 			),
       
   296 			'letterSpacing'  => array(
       
   297 				'property_keys' => array(
       
   298 					'default' => 'letter-spacing',
       
   299 				),
       
   300 				'path'          => array( 'typography', 'letterSpacing' ),
       
   301 			),
       
   302 		),
       
   303 	);
       
   304 
       
   305 	/**
       
   306 	 * Util: Extracts the slug in kebab case from a preset string,
       
   307 	 * e.g. `heavenly-blue` from `var:preset|color|heavenlyBlue`.
       
   308 	 *
       
   309 	 * @since 6.1.0
       
   310 	 *
       
   311 	 * @param string $style_value  A single CSS preset value.
       
   312 	 * @param string $property_key The CSS property that is the second element of the preset string.
       
   313 	 *                             Used for matching.
       
   314 	 * @return string The slug, or empty string if not found.
       
   315 	 */
       
   316 	protected static function get_slug_from_preset_value( $style_value, $property_key ) {
       
   317 		if ( is_string( $style_value ) && is_string( $property_key )
       
   318 			&& str_contains( $style_value, "var:preset|{$property_key}|" )
       
   319 		) {
       
   320 			$index_to_splice = strrpos( $style_value, '|' ) + 1;
       
   321 			return _wp_to_kebab_case( substr( $style_value, $index_to_splice ) );
       
   322 		}
       
   323 		return '';
       
   324 	}
       
   325 
       
   326 	/**
       
   327 	 * Util: Generates a CSS var string, e.g. `var(--wp--preset--color--background)`
       
   328 	 * from a preset string such as `var:preset|space|50`.
       
   329 	 *
       
   330 	 * @since 6.1.0
       
   331 	 *
       
   332 	 * @param string   $style_value  A single CSS preset value.
       
   333 	 * @param string[] $css_vars     An associate array of CSS var patterns
       
   334 	 *                               used to generate the var string.
       
   335 	 * @return string The CSS var, or an empty string if no match for slug found.
       
   336 	 */
       
   337 	protected static function get_css_var_value( $style_value, $css_vars ) {
       
   338 		foreach ( $css_vars as $property_key => $css_var_pattern ) {
       
   339 			$slug = static::get_slug_from_preset_value( $style_value, $property_key );
       
   340 			if ( static::is_valid_style_value( $slug ) ) {
       
   341 				$var = strtr(
       
   342 					$css_var_pattern,
       
   343 					array( '$slug' => $slug )
       
   344 				);
       
   345 				return "var($var)";
       
   346 			}
       
   347 		}
       
   348 		return '';
       
   349 	}
       
   350 
       
   351 	/**
       
   352 	 * Util: Checks whether an incoming block style value is valid.
       
   353 	 *
       
   354 	 * @since 6.1.0
       
   355 	 *
       
   356 	 * @param string $style_value A single CSS preset value.
       
   357 	 * @return bool
       
   358 	 */
       
   359 	protected static function is_valid_style_value( $style_value ) {
       
   360 		return '0' === $style_value || ! empty( $style_value );
       
   361 	}
       
   362 
       
   363 	/**
       
   364 	 * Stores a CSS rule using the provided CSS selector and CSS declarations.
       
   365 	 *
       
   366 	 * @since 6.1.0
       
   367 	 * @since 6.6.0 Added the `$rules_group` parameter.
       
   368 	 *
       
   369 	 * @param string   $store_name       A valid store key.
       
   370 	 * @param string   $css_selector     When a selector is passed, the function will return
       
   371 	 *                                   a full CSS rule `$selector { ...rules }`
       
   372 	 *                                   otherwise a concatenated string of properties and values.
       
   373 	 * @param string[] $css_declarations An associative array of CSS definitions,
       
   374 	 *                                   e.g. `array( "$property" => "$value", "$property" => "$value" )`.
       
   375 	 * @param string $rules_group        Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
       
   376 	 *                                   such as `@media (min-width: 80rem)` or `@layer module`.
       
   377 	 */
       
   378 	public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) {
       
   379 		if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) {
       
   380 			return;
       
   381 		}
       
   382 		static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations );
       
   383 	}
       
   384 
       
   385 	/**
       
   386 	 * Returns a store by store key.
       
   387 	 *
       
   388 	 * @since 6.1.0
       
   389 	 *
       
   390 	 * @param string $store_name A store key.
       
   391 	 * @return WP_Style_Engine_CSS_Rules_Store|null
       
   392 	 */
       
   393 	public static function get_store( $store_name ) {
       
   394 		return WP_Style_Engine_CSS_Rules_Store::get_store( $store_name );
       
   395 	}
       
   396 
       
   397 	/**
       
   398 	 * Returns classnames and CSS based on the values in a styles object.
       
   399 	 *
       
   400 	 * Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA.
       
   401 	 *
       
   402 	 * @since 6.1.0
       
   403 	 *
       
   404 	 * @param array $block_styles The style object.
       
   405 	 * @param array $options      {
       
   406 	 *     Optional. An array of options. Default empty array.
       
   407 	 *
       
   408 	 *     @type bool        $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns,
       
   409 	 *                                                   e.g. `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`,
       
   410 	 *                                                   to `var( --wp--preset--* )` values. Default false.
       
   411 	 *     @type string      $selector                   Optional. When a selector is passed,
       
   412 	 *                                                   the value of `$css` in the return value will comprise
       
   413 	 *                                                   a full CSS rule `$selector { ...$css_declarations }`,
       
   414 	 *                                                   otherwise, the value will be a concatenated string
       
   415 	 *                                                   of CSS declarations.
       
   416 	 * }
       
   417 	 * @return array {
       
   418 	 *     @type string[] $classnames   Array of class names.
       
   419 	 *     @type string[] $declarations An associative array of CSS definitions,
       
   420 	 *                                  e.g. `array( "$property" => "$value", "$property" => "$value" )`.
       
   421 	 * }
       
   422 	 */
       
   423 	public static function parse_block_styles( $block_styles, $options ) {
       
   424 		$parsed_styles = array(
       
   425 			'classnames'   => array(),
       
   426 			'declarations' => array(),
       
   427 		);
       
   428 		if ( empty( $block_styles ) || ! is_array( $block_styles ) ) {
       
   429 			return $parsed_styles;
       
   430 		}
       
   431 
       
   432 		// Collect CSS and classnames.
       
   433 		foreach ( static::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) {
       
   434 			if ( empty( $block_styles[ $definition_group_key ] ) ) {
       
   435 				continue;
       
   436 			}
       
   437 			foreach ( $definition_group_style as $style_definition ) {
       
   438 				$style_value = _wp_array_get( $block_styles, $style_definition['path'], null );
       
   439 
       
   440 				if ( ! static::is_valid_style_value( $style_value ) ) {
       
   441 					continue;
       
   442 				}
       
   443 
       
   444 				$parsed_styles['classnames']   = array_merge( $parsed_styles['classnames'], static::get_classnames( $style_value, $style_definition ) );
       
   445 				$parsed_styles['declarations'] = array_merge( $parsed_styles['declarations'], static::get_css_declarations( $style_value, $style_definition, $options ) );
       
   446 			}
       
   447 		}
       
   448 
       
   449 		return $parsed_styles;
       
   450 	}
       
   451 
       
   452 	/**
       
   453 	 * Returns classnames, and generates classname(s) from a CSS preset property pattern,
       
   454 	 * e.g. `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`.
       
   455 	 *
       
   456 	 * @since 6.1.0
       
   457 	 *
       
   458 	 * @param string $style_value      A single raw style value or CSS preset property
       
   459 	 *                                 from the `$block_styles` array.
       
   460 	 * @param array  $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
       
   461 	 * @return string[] An array of CSS classnames, or empty array if there are none.
       
   462 	 */
       
   463 	protected static function get_classnames( $style_value, $style_definition ) {
       
   464 		if ( empty( $style_value ) ) {
       
   465 			return array();
       
   466 		}
       
   467 
       
   468 		$classnames = array();
       
   469 		if ( ! empty( $style_definition['classnames'] ) ) {
       
   470 			foreach ( $style_definition['classnames'] as $classname => $property_key ) {
       
   471 				if ( true === $property_key ) {
       
   472 					$classnames[] = $classname;
       
   473 					continue;
       
   474 				}
       
   475 
       
   476 				$slug = static::get_slug_from_preset_value( $style_value, $property_key );
       
   477 
       
   478 				if ( $slug ) {
       
   479 					/*
       
   480 					 * Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA.
       
   481 					 * One day, if there are no stored schemata, we could allow custom patterns or
       
   482 					 * generate classnames based on other properties
       
   483 					 * such as a path or a value or a prefix passed in options.
       
   484 					 */
       
   485 					$classnames[] = strtr( $classname, array( '$slug' => $slug ) );
       
   486 				}
       
   487 			}
       
   488 		}
       
   489 
       
   490 		return $classnames;
       
   491 	}
       
   492 
       
   493 	/**
       
   494 	 * Returns an array of CSS declarations based on valid block style values.
       
   495 	 *
       
   496 	 * @since 6.1.0
       
   497 	 *
       
   498 	 * @param mixed $style_value      A single raw style value from $block_styles array.
       
   499 	 * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
       
   500 	 * @param array $options          {
       
   501 	 *     Optional. An array of options. Default empty array.
       
   502 	 *
       
   503 	 *     @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns,
       
   504 	 *                                            e.g. `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`,
       
   505 	 *                                            to `var( --wp--preset--* )` values. Default false.
       
   506 	 * }
       
   507 	 * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`.
       
   508 	 */
       
   509 	protected static function get_css_declarations( $style_value, $style_definition, $options = array() ) {
       
   510 		if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) {
       
   511 			return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options );
       
   512 		}
       
   513 
       
   514 		$css_declarations     = array();
       
   515 		$style_property_keys  = $style_definition['property_keys'];
       
   516 		$should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames'];
       
   517 
       
   518 		/*
       
   519 		 * Build CSS var values from `var:preset|<PRESET_TYPE>|<PRESET_SLUG>` values, e.g, `var(--wp--css--rule-slug )`.
       
   520 		 * Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition.
       
   521 		 */
       
   522 		if ( is_string( $style_value ) && str_contains( $style_value, 'var:' ) ) {
       
   523 			if ( ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) {
       
   524 				$css_var = static::get_css_var_value( $style_value, $style_definition['css_vars'] );
       
   525 				if ( static::is_valid_style_value( $css_var ) ) {
       
   526 					$css_declarations[ $style_property_keys['default'] ] = $css_var;
       
   527 				}
       
   528 			}
       
   529 			return $css_declarations;
       
   530 		}
       
   531 
       
   532 		/*
       
   533 		 * Default rule builder.
       
   534 		 * If the input contains an array, assume box model-like properties
       
   535 		 * for styles such as margins and padding.
       
   536 		 */
       
   537 		if ( is_array( $style_value ) ) {
       
   538 			// Bail out early if the `'individual'` property is not defined.
       
   539 			if ( ! isset( $style_property_keys['individual'] ) ) {
       
   540 				return $css_declarations;
       
   541 			}
       
   542 
       
   543 			foreach ( $style_value as $key => $value ) {
       
   544 				if ( is_string( $value ) && str_contains( $value, 'var:' ) && ! $should_skip_css_vars && ! empty( $style_definition['css_vars'] ) ) {
       
   545 					$value = static::get_css_var_value( $value, $style_definition['css_vars'] );
       
   546 				}
       
   547 
       
   548 				$individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) );
       
   549 
       
   550 				if ( $individual_property && static::is_valid_style_value( $value ) ) {
       
   551 					$css_declarations[ $individual_property ] = $value;
       
   552 				}
       
   553 			}
       
   554 
       
   555 			return $css_declarations;
       
   556 		}
       
   557 
       
   558 		$css_declarations[ $style_property_keys['default'] ] = $style_value;
       
   559 		return $css_declarations;
       
   560 	}
       
   561 
       
   562 	/**
       
   563 	 * Style value parser that returns a CSS definition array comprising style properties
       
   564 	 * that have keys representing individual style properties, otherwise known as longhand CSS properties.
       
   565 	 *
       
   566 	 * Example:
       
   567 	 *
       
   568 	 *     "$style_property-$individual_feature: $value;"
       
   569 	 *
       
   570 	 * Which could represent the following:
       
   571 	 *
       
   572 	 *     "border-{top|right|bottom|left}-{color|width|style}: {value};"
       
   573 	 *
       
   574 	 * or:
       
   575 	 *
       
   576 	 *     "border-image-{outset|source|width|repeat|slice}: {value};"
       
   577 	 *
       
   578 	 * @since 6.1.0
       
   579 	 *
       
   580 	 * @param array $style_value                    A single raw style value from `$block_styles` array.
       
   581 	 * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA
       
   582 	 *                                              representing an individual property of a CSS property,
       
   583 	 *                                              e.g. 'top' in 'border-top'.
       
   584 	 * @param array $options                        {
       
   585 	 *     Optional. An array of options. Default empty array.
       
   586 	 *
       
   587 	 *     @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns,
       
   588 	 *                                            e.g. `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`,
       
   589 	 *                                            to `var( --wp--preset--* )` values. Default false.
       
   590 	 * }
       
   591 	 * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`.
       
   592 	 */
       
   593 	protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options = array() ) {
       
   594 		if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) {
       
   595 			return array();
       
   596 		}
       
   597 
       
   598 		/*
       
   599 		 * The first item in $individual_property_definition['path'] array
       
   600 		 * tells us the style property, e.g. "border". We use this to get a corresponding
       
   601 		 * CSS style definition such as "color" or "width" from the same group.
       
   602 		 *
       
   603 		 * The second item in $individual_property_definition['path'] array
       
   604 		 * refers to the individual property marker, e.g. "top".
       
   605 		 */
       
   606 		$definition_group_key    = $individual_property_definition['path'][0];
       
   607 		$individual_property_key = $individual_property_definition['path'][1];
       
   608 		$should_skip_css_vars    = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames'];
       
   609 		$css_declarations        = array();
       
   610 
       
   611 		foreach ( $style_value as $css_property => $value ) {
       
   612 			if ( empty( $value ) ) {
       
   613 				continue;
       
   614 			}
       
   615 
       
   616 			// Build a path to the individual rules in definitions.
       
   617 			$style_definition_path = array( $definition_group_key, $css_property );
       
   618 			$style_definition      = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition_path, null );
       
   619 
       
   620 			if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) {
       
   621 				// Set a CSS var if there is a valid preset value.
       
   622 				if ( is_string( $value ) && str_contains( $value, 'var:' )
       
   623 					&& ! $should_skip_css_vars && ! empty( $individual_property_definition['css_vars'] )
       
   624 				) {
       
   625 					$value = static::get_css_var_value( $value, $individual_property_definition['css_vars'] );
       
   626 				}
       
   627 
       
   628 				$individual_css_property = sprintf( $style_definition['property_keys']['individual'], $individual_property_key );
       
   629 
       
   630 				$css_declarations[ $individual_css_property ] = $value;
       
   631 			}
       
   632 		}
       
   633 		return $css_declarations;
       
   634 	}
       
   635 
       
   636 	/**
       
   637 	 * Style value parser that constructs a CSS definition array comprising a single CSS property and value.
       
   638 	 * If the provided value is an array containing a `url` property, the function will return a CSS definition array
       
   639 	 * with a single property and value, with `url` escaped and injected into a CSS `url()` function,
       
   640 	 * e.g., array( 'background-image' => "url( '...' )" ).
       
   641 	 *
       
   642 	 * @since 6.4.0
       
   643 	 *
       
   644 	 * @param array $style_value      A single raw style value from $block_styles array.
       
   645 	 * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
       
   646 	 * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ).
       
   647 	 */
       
   648 	protected static function get_url_or_value_css_declaration( $style_value, $style_definition ) {
       
   649 		if ( empty( $style_value ) ) {
       
   650 			return array();
       
   651 		}
       
   652 
       
   653 		$css_declarations = array();
       
   654 
       
   655 		if ( isset( $style_definition['property_keys']['default'] ) ) {
       
   656 			$value = null;
       
   657 
       
   658 			if ( ! empty( $style_value['url'] ) ) {
       
   659 				$value = "url('" . $style_value['url'] . "')";
       
   660 			} elseif ( is_string( $style_value ) ) {
       
   661 				$value = $style_value;
       
   662 			}
       
   663 
       
   664 			if ( null !== $value ) {
       
   665 				$css_declarations[ $style_definition['property_keys']['default'] ] = $value;
       
   666 			}
       
   667 		}
       
   668 
       
   669 		return $css_declarations;
       
   670 	}
       
   671 
       
   672 	/**
       
   673 	 * Returns compiled CSS from CSS declarations.
       
   674 	 *
       
   675 	 * @since 6.1.0
       
   676 	 *
       
   677 	 * @param string[] $css_declarations An associative array of CSS definitions,
       
   678 	 *                                   e.g. `array( "$property" => "$value", "$property" => "$value" )`.
       
   679 	 * @param string   $css_selector     When a selector is passed, the function will return
       
   680 	 *                                   a full CSS rule `$selector { ...rules }`,
       
   681 	 *                                   otherwise a concatenated string of properties and values.
       
   682 	 * @return string A compiled CSS string.
       
   683 	 */
       
   684 	public static function compile_css( $css_declarations, $css_selector ) {
       
   685 		if ( empty( $css_declarations ) || ! is_array( $css_declarations ) ) {
       
   686 			return '';
       
   687 		}
       
   688 
       
   689 		// Return an entire rule if there is a selector.
       
   690 		if ( $css_selector ) {
       
   691 			$css_rule = new WP_Style_Engine_CSS_Rule( $css_selector, $css_declarations );
       
   692 			return $css_rule->get_css();
       
   693 		}
       
   694 
       
   695 		$css_declarations = new WP_Style_Engine_CSS_Declarations( $css_declarations );
       
   696 		return $css_declarations->get_declarations_string();
       
   697 	}
       
   698 
       
   699 	/**
       
   700 	 * Returns a compiled stylesheet from stored CSS rules.
       
   701 	 *
       
   702 	 * @since 6.1.0
       
   703 	 *
       
   704 	 * @param WP_Style_Engine_CSS_Rule[] $css_rules An array of WP_Style_Engine_CSS_Rule objects
       
   705 	 *                                              from a store or otherwise.
       
   706 	 * @param array                      $options   {
       
   707 	 *     Optional. An array of options. Default empty array.
       
   708 	 *
       
   709 	 *     @type string|null $context  An identifier describing the origin of the style object,
       
   710 	 *                                 e.g. 'block-supports' or 'global-styles'. Default 'block-supports'.
       
   711 	 *                                 When set, the style engine will attempt to store the CSS rules.
       
   712 	 *     @type bool        $optimize Whether to optimize the CSS output, e.g. combine rules.
       
   713 	 *                                 Default false.
       
   714 	 *     @type bool        $prettify Whether to add new lines and indents to output.
       
   715 	 *                                 Defaults to whether the `SCRIPT_DEBUG` constant is defined.
       
   716 	 * }
       
   717 	 * @return string A compiled stylesheet from stored CSS rules.
       
   718 	 */
       
   719 	public static function compile_stylesheet_from_css_rules( $css_rules, $options = array() ) {
       
   720 		$processor = new WP_Style_Engine_Processor();
       
   721 		$processor->add_rules( $css_rules );
       
   722 		return $processor->get_css( $options );
       
   723 	}
       
   724 }