ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-theme-json.php (33739B)


      1 <?php
      2 /**
      3  * WP_Theme_JSON class
      4  *
      5  * @package WordPress
      6  * @subpackage Theme
      7  * @since 5.8.0
      8  */
      9 
     10 /**
     11  * Class that encapsulates the processing of structures that adhere to the theme.json spec.
     12  *
     13  * @access private
     14  */
     15 class WP_Theme_JSON {
     16 
     17 	/**
     18 	 * Container of data in theme.json format.
     19 	 *
     20 	 * @since 5.8.0
     21 	 * @var array
     22 	 */
     23 	private $theme_json = null;
     24 
     25 	/**
     26 	 * Holds block metadata extracted from block.json
     27 	 * to be shared among all instances so we don't
     28 	 * process it twice.
     29 	 *
     30 	 * @since 5.8.0
     31 	 * @var array
     32 	 */
     33 	private static $blocks_metadata = null;
     34 
     35 	/**
     36 	 * The CSS selector for the top-level styles.
     37 	 *
     38 	 * @since 5.8.0
     39 	 * @var string
     40 	 */
     41 	const ROOT_BLOCK_SELECTOR = 'body';
     42 
     43 	/**
     44 	 * The sources of data this object can represent.
     45 	 *
     46 	 * @since 5.8.0
     47 	 * @var array
     48 	 */
     49 	const VALID_ORIGINS = array(
     50 		'core',
     51 		'theme',
     52 		'user',
     53 	);
     54 
     55 	/**
     56 	 * Presets are a set of values that serve
     57 	 * to bootstrap some styles: colors, font sizes, etc.
     58 	 *
     59 	 * They are a unkeyed array of values such as:
     60 	 *
     61 	 * ```php
     62 	 * array(
     63 	 *   array(
     64 	 *     'slug'      => 'unique-name-within-the-set',
     65 	 *     'name'      => 'Name for the UI',
     66 	 *     <value_key> => 'value'
     67 	 *   ),
     68 	 * )
     69 	 * ```
     70 	 *
     71 	 * This contains the necessary metadata to process them:
     72 	 *
     73 	 * - path          => where to find the preset within the settings section
     74 	 *
     75 	 * - value_key     => the key that represents the value
     76 	 *
     77 	 * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
     78 	 *                   --wp--preset--<preset_infix>--<slug>: <preset_value>
     79 	 *
     80 	 * - classes      => array containing a structure with the classes to
     81 	 *                   generate for the presets. Each class should have
     82 	 *                   the class suffix and the property name. Example:
     83 	 *
     84 	 *                   .has-<slug>-<class_suffix> {
     85 	 *                       <property_name>: <preset_value>
     86 	 *                   }
     87 	 *
     88 	 * @since 5.8.0
     89 	 * @var array
     90 	 */
     91 	const PRESETS_METADATA = array(
     92 		array(
     93 			'path'          => array( 'color', 'palette' ),
     94 			'value_key'     => 'color',
     95 			'css_var_infix' => 'color',
     96 			'classes'       => array(
     97 				array(
     98 					'class_suffix'  => 'color',
     99 					'property_name' => 'color',
    100 				),
    101 				array(
    102 					'class_suffix'  => 'background-color',
    103 					'property_name' => 'background-color',
    104 				),
    105 			),
    106 		),
    107 		array(
    108 			'path'          => array( 'color', 'gradients' ),
    109 			'value_key'     => 'gradient',
    110 			'css_var_infix' => 'gradient',
    111 			'classes'       => array(
    112 				array(
    113 					'class_suffix'  => 'gradient-background',
    114 					'property_name' => 'background',
    115 				),
    116 			),
    117 		),
    118 		array(
    119 			'path'          => array( 'typography', 'fontSizes' ),
    120 			'value_key'     => 'size',
    121 			'css_var_infix' => 'font-size',
    122 			'classes'       => array(
    123 				array(
    124 					'class_suffix'  => 'font-size',
    125 					'property_name' => 'font-size',
    126 				),
    127 			),
    128 		),
    129 	);
    130 
    131 	/**
    132 	 * Metadata for style properties.
    133 	 *
    134 	 * Each property declares:
    135 	 *
    136 	 * - 'value': path to the value in theme.json and block attributes.
    137 	 *
    138 	 * @since 5.8.0
    139 	 * @var array
    140 	 */
    141 	const PROPERTIES_METADATA = array(
    142 		'background'       => array(
    143 			'value' => array( 'color', 'gradient' ),
    144 		),
    145 		'background-color' => array(
    146 			'value' => array( 'color', 'background' ),
    147 		),
    148 		'color'            => array(
    149 			'value' => array( 'color', 'text' ),
    150 		),
    151 		'font-size'        => array(
    152 			'value' => array( 'typography', 'fontSize' ),
    153 		),
    154 		'line-height'      => array(
    155 			'value' => array( 'typography', 'lineHeight' ),
    156 		),
    157 		'margin'           => array(
    158 			'value'      => array( 'spacing', 'margin' ),
    159 			'properties' => array( 'top', 'right', 'bottom', 'left' ),
    160 		),
    161 		'padding'          => array(
    162 			'value'      => array( 'spacing', 'padding' ),
    163 			'properties' => array( 'top', 'right', 'bottom', 'left' ),
    164 		),
    165 	);
    166 
    167 	/**
    168 	 * @since 5.8.0
    169 	 * @var array
    170 	 */
    171 	const ALLOWED_TOP_LEVEL_KEYS = array(
    172 		'settings',
    173 		'styles',
    174 		'version',
    175 	);
    176 
    177 	/**
    178 	 * @since 5.8.0
    179 	 * @var array
    180 	 */
    181 	const ALLOWED_SETTINGS = array(
    182 		'border'     => array(
    183 			'customRadius' => null,
    184 		),
    185 		'color'      => array(
    186 			'custom'         => null,
    187 			'customDuotone'  => null,
    188 			'customGradient' => null,
    189 			'duotone'        => null,
    190 			'gradients'      => null,
    191 			'link'           => null,
    192 			'palette'        => null,
    193 		),
    194 		'custom'     => null,
    195 		'layout'     => array(
    196 			'contentSize' => null,
    197 			'wideSize'    => null,
    198 		),
    199 		'spacing'    => array(
    200 			'customMargin'  => null,
    201 			'customPadding' => null,
    202 			'units'         => null,
    203 		),
    204 		'typography' => array(
    205 			'customFontSize'   => null,
    206 			'customLineHeight' => null,
    207 			'dropCap'          => null,
    208 			'fontSizes'        => null,
    209 		),
    210 	);
    211 
    212 	/**
    213 	 * @since 5.8.0
    214 	 * @var array
    215 	 */
    216 	const ALLOWED_STYLES = array(
    217 		'border'     => array(
    218 			'radius' => null,
    219 		),
    220 		'color'      => array(
    221 			'background' => null,
    222 			'gradient'   => null,
    223 			'text'       => null,
    224 		),
    225 		'spacing'    => array(
    226 			'margin'  => array(
    227 				'top'    => null,
    228 				'right'  => null,
    229 				'bottom' => null,
    230 				'left'   => null,
    231 			),
    232 			'padding' => array(
    233 				'bottom' => null,
    234 				'left'   => null,
    235 				'right'  => null,
    236 				'top'    => null,
    237 			),
    238 		),
    239 		'typography' => array(
    240 			'fontSize'   => null,
    241 			'lineHeight' => null,
    242 		),
    243 	);
    244 
    245 	/**
    246 	 * @since 5.8.0
    247 	 * @var array
    248 	 */
    249 	const ELEMENTS = array(
    250 		'link' => 'a',
    251 		'h1'   => 'h1',
    252 		'h2'   => 'h2',
    253 		'h3'   => 'h3',
    254 		'h4'   => 'h4',
    255 		'h5'   => 'h5',
    256 		'h6'   => 'h6',
    257 	);
    258 
    259 	/**
    260 	 * @since 5.8.0
    261 	 * @var int
    262 	 */
    263 	const LATEST_SCHEMA = 1;
    264 
    265 	/**
    266 	 * Constructor.
    267 	 *
    268 	 * @since 5.8.0
    269 	 *
    270 	 * @param array $theme_json A structure that follows the theme.json schema.
    271 	 * @param string $origin    Optional. What source of data this object represents.
    272 	 *                          One of 'core', 'theme', or 'user'. Default 'theme'.
    273 	 */
    274 	public function __construct( $theme_json = array(), $origin = 'theme' ) {
    275 		if ( ! in_array( $origin, self::VALID_ORIGINS, true ) ) {
    276 			$origin = 'theme';
    277 		}
    278 
    279 		if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) {
    280 			$this->theme_json = array();
    281 			return;
    282 		}
    283 
    284 		$this->theme_json = self::sanitize( $theme_json );
    285 
    286 		// Internally, presets are keyed by origin.
    287 		$nodes = self::get_setting_nodes( $this->theme_json );
    288 		foreach ( $nodes as $node ) {
    289 			foreach ( self::PRESETS_METADATA as $preset ) {
    290 				$path   = array_merge( $node['path'], $preset['path'] );
    291 				$preset = _wp_array_get( $this->theme_json, $path, null );
    292 				if ( null !== $preset ) {
    293 					_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
    294 				}
    295 			}
    296 		}
    297 	}
    298 
    299 	/**
    300 	 * Sanitizes the input according to the schemas.
    301 	 *
    302 	 * @since 5.8.0
    303 	 *
    304 	 * @param array $input Structure to sanitize.
    305 	 * @return array The sanitized output.
    306 	 */
    307 	private static function sanitize( $input ) {
    308 		$output = array();
    309 
    310 		if ( ! is_array( $input ) ) {
    311 			return $output;
    312 		}
    313 
    314 		$allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS;
    315 		$allowed_settings       = self::ALLOWED_SETTINGS;
    316 		$allowed_styles         = self::ALLOWED_STYLES;
    317 		$allowed_blocks         = array_keys( self::get_blocks_metadata() );
    318 		$allowed_elements       = array_keys( self::ELEMENTS );
    319 
    320 		$output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
    321 
    322 		// Build the schema.
    323 		$schema                 = array();
    324 		$schema_styles_elements = array();
    325 		foreach ( $allowed_elements as $element ) {
    326 			$schema_styles_elements[ $element ] = $allowed_styles;
    327 		}
    328 		$schema_styles_blocks   = array();
    329 		$schema_settings_blocks = array();
    330 		foreach ( $allowed_blocks as $block ) {
    331 			$schema_settings_blocks[ $block ]           = $allowed_settings;
    332 			$schema_styles_blocks[ $block ]             = $allowed_styles;
    333 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
    334 		}
    335 		$schema['styles']             = $allowed_styles;
    336 		$schema['styles']['blocks']   = $schema_styles_blocks;
    337 		$schema['styles']['elements'] = $schema_styles_elements;
    338 		$schema['settings']           = $allowed_settings;
    339 		$schema['settings']['blocks'] = $schema_settings_blocks;
    340 
    341 		// Remove anything that's not present in the schema.
    342 		foreach ( array( 'styles', 'settings' ) as $subtree ) {
    343 			if ( ! isset( $input[ $subtree ] ) ) {
    344 				continue;
    345 			}
    346 
    347 			if ( ! is_array( $input[ $subtree ] ) ) {
    348 				unset( $output[ $subtree ] );
    349 				continue;
    350 			}
    351 
    352 			$result = self::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
    353 
    354 			if ( empty( $result ) ) {
    355 				unset( $output[ $subtree ] );
    356 			} else {
    357 				$output[ $subtree ] = $result;
    358 			}
    359 		}
    360 
    361 		return $output;
    362 	}
    363 
    364 	/**
    365 	 * Returns the metadata for each block.
    366 	 *
    367 	 * Example:
    368 	 *
    369 	 *     {
    370 	 *       'core/paragraph': {
    371 	 *         'selector': 'p',
    372 	 *         'elements': {
    373 	 *           'link' => 'link selector',
    374 	 *           'etc'  => 'element selector'
    375 	 *         }
    376 	 *       },
    377 	 *       'core/heading': {
    378 	 *         'selector': 'h1',
    379 	 *         'elements': {}
    380 	 *       }
    381 	 *       'core/group': {
    382 	 *         'selector': '.wp-block-group',
    383 	 *         'elements': {}
    384 	 *       }
    385 	 *     }
    386 	 *
    387 	 * @since 5.8.0
    388 	 *
    389 	 * @return array Block metadata.
    390 	 */
    391 	private static function get_blocks_metadata() {
    392 		if ( null !== self::$blocks_metadata ) {
    393 			return self::$blocks_metadata;
    394 		}
    395 
    396 		self::$blocks_metadata = array();
    397 
    398 		$registry = WP_Block_Type_Registry::get_instance();
    399 		$blocks   = $registry->get_all_registered();
    400 		foreach ( $blocks as $block_name => $block_type ) {
    401 			if (
    402 				isset( $block_type->supports['__experimentalSelector'] ) &&
    403 				is_string( $block_type->supports['__experimentalSelector'] )
    404 			) {
    405 				self::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
    406 			} else {
    407 				self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
    408 			}
    409 
    410 			/*
    411 			 * Assign defaults, then overwrite those that the block sets by itself.
    412 			 * If the block selector is compounded, will append the element to each
    413 			 * individual block selector.
    414 			 */
    415 			$block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] );
    416 			foreach ( self::ELEMENTS as $el_name => $el_selector ) {
    417 				$element_selector = array();
    418 				foreach ( $block_selectors as $selector ) {
    419 					$element_selector[] = $selector . ' ' . $el_selector;
    420 				}
    421 				self::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
    422 			}
    423 		}
    424 
    425 		return self::$blocks_metadata;
    426 	}
    427 
    428 	/**
    429 	 * Given a tree, removes the keys that are not present in the schema.
    430 	 *
    431 	 * It is recursive and modifies the input in-place.
    432 	 *
    433 	 * @since 5.8.0
    434 	 *
    435 	 * @param array $tree   Input to process.
    436 	 * @param array $schema Schema to adhere to.
    437 	 * @return array Returns the modified $tree.
    438 	 */
    439 	private static function remove_keys_not_in_schema( $tree, $schema ) {
    440 		$tree = array_intersect_key( $tree, $schema );
    441 
    442 		foreach ( $schema as $key => $data ) {
    443 			if ( ! isset( $tree[ $key ] ) ) {
    444 				continue;
    445 			}
    446 
    447 			if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
    448 				$tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
    449 
    450 				if ( empty( $tree[ $key ] ) ) {
    451 					unset( $tree[ $key ] );
    452 				}
    453 			} elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
    454 				unset( $tree[ $key ] );
    455 			}
    456 		}
    457 
    458 		return $tree;
    459 	}
    460 
    461 	/**
    462 	 * Returns the existing settings for each block.
    463 	 *
    464 	 * Example:
    465 	 *
    466 	 *     {
    467 	 *       'root': {
    468 	 *         'color': {
    469 	 *           'custom': true
    470 	 *         }
    471 	 *       },
    472 	 *       'core/paragraph': {
    473 	 *         'spacing': {
    474 	 *           'customPadding': true
    475 	 *         }
    476 	 *       }
    477 	 *     }
    478 	 *
    479 	 * @since 5.8.0
    480 	 *
    481 	 * @return array Settings per block.
    482 	 */
    483 	public function get_settings() {
    484 		if ( ! isset( $this->theme_json['settings'] ) ) {
    485 			return array();
    486 		} else {
    487 			return $this->theme_json['settings'];
    488 		}
    489 	}
    490 
    491 	/**
    492 	 * Returns the stylesheet that results of processing
    493 	 * the theme.json structure this object represents.
    494 	 *
    495 	 * @since 5.8.0
    496 	 *
    497 	 * @param string $type Optional. Type of stylesheet we want. Accepts 'all',
    498 	 *                     'block_styles', and 'css_variables'. Default 'all'.
    499 	 * @return string Stylesheet.
    500 	 */
    501 	public function get_stylesheet( $type = 'all' ) {
    502 		$blocks_metadata = self::get_blocks_metadata();
    503 		$style_nodes     = self::get_style_nodes( $this->theme_json, $blocks_metadata );
    504 		$setting_nodes   = self::get_setting_nodes( $this->theme_json, $blocks_metadata );
    505 
    506 		switch ( $type ) {
    507 			case 'block_styles':
    508 				return $this->get_block_styles( $style_nodes, $setting_nodes );
    509 			case 'css_variables':
    510 				return $this->get_css_variables( $setting_nodes );
    511 			default:
    512 				return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes );
    513 		}
    514 
    515 	}
    516 
    517 	/**
    518 	 * Converts each style section into a list of rulesets
    519 	 * containing the block styles to be appended to the stylesheet.
    520 	 *
    521 	 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
    522 	 *
    523 	 * For each section this creates a new ruleset such as:
    524 	 *
    525 	 *   block-selector {
    526 	 *     style-property-one: value;
    527 	 *   }
    528 	 *
    529 	 * Additionally, it'll also create new rulesets
    530 	 * as classes for each preset value such as:
    531 	 *
    532 	 *     .has-value-color {
    533 	 *       color: value;
    534 	 *     }
    535 	 *
    536 	 *     .has-value-background-color {
    537 	 *       background-color: value;
    538 	 *     }
    539 	 *
    540 	 *     .has-value-font-size {
    541 	 *       font-size: value;
    542 	 *     }
    543 	 *
    544 	 *     .has-value-gradient-background {
    545 	 *       background: value;
    546 	 *     }
    547 	 *
    548 	 *     p.has-value-gradient-background {
    549 	 *       background: value;
    550 	 *     }
    551 	 *
    552 	 * @since 5.8.0
    553 	 *
    554 	 * @param array $style_nodes   Nodes with styles.
    555 	 * @param array $setting_nodes Nodes with settings.
    556 	 * @return string The new stylesheet.
    557 	 */
    558 	private function get_block_styles( $style_nodes, $setting_nodes ) {
    559 		$block_rules = '';
    560 		foreach ( $style_nodes as $metadata ) {
    561 			if ( null === $metadata['selector'] ) {
    562 				continue;
    563 			}
    564 
    565 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    566 			$selector     = $metadata['selector'];
    567 			$declarations = self::compute_style_properties( $node );
    568 			$block_rules .= self::to_ruleset( $selector, $declarations );
    569 		}
    570 
    571 		$preset_rules = '';
    572 		foreach ( $setting_nodes as $metadata ) {
    573 			if ( null === $metadata['selector'] ) {
    574 				continue;
    575 			}
    576 
    577 			$selector      = $metadata['selector'];
    578 			$node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    579 			$preset_rules .= self::compute_preset_classes( $node, $selector );
    580 		}
    581 
    582 		return $block_rules . $preset_rules;
    583 	}
    584 
    585 	/**
    586 	 * Converts each styles section into a list of rulesets
    587 	 * to be appended to the stylesheet.
    588 	 * These rulesets contain all the css variables (custom variables and preset variables).
    589 	 *
    590 	 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
    591 	 *
    592 	 * For each section this creates a new ruleset such as:
    593 	 *
    594 	 *     block-selector {
    595 	 *       --wp--preset--category--slug: value;
    596 	 *       --wp--custom--variable: value;
    597 	 *     }
    598 	 *
    599 	 * @since 5.8.0
    600 	 *
    601 	 * @param array $nodes Nodes with settings.
    602 	 * @return string The new stylesheet.
    603 	 */
    604 	private function get_css_variables( $nodes ) {
    605 		$stylesheet = '';
    606 		foreach ( $nodes as $metadata ) {
    607 			if ( null === $metadata['selector'] ) {
    608 				continue;
    609 			}
    610 
    611 			$selector = $metadata['selector'];
    612 
    613 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    614 			$declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );
    615 
    616 			$stylesheet .= self::to_ruleset( $selector, $declarations );
    617 		}
    618 
    619 		return $stylesheet;
    620 	}
    621 
    622 	/**
    623 	 * Given a selector and a declaration list,
    624 	 * creates the corresponding ruleset.
    625 	 *
    626 	 * @since 5.8.0
    627 	 *
    628 	 * @param string $selector     CSS selector.
    629 	 * @param array  $declarations List of declarations.
    630 	 * @return string CSS ruleset.
    631 	 */
    632 	private static function to_ruleset( $selector, $declarations ) {
    633 		if ( empty( $declarations ) ) {
    634 			return '';
    635 		}
    636 
    637 		$declaration_block = array_reduce(
    638 			$declarations,
    639 			function ( $carry, $element ) {
    640 				return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
    641 			''
    642 		);
    643 
    644 		return $selector . '{' . $declaration_block . '}';
    645 	}
    646 
    647 	/**
    648 	 * Function that appends a sub-selector to a existing one.
    649 	 *
    650 	 * Given the compounded $selector "h1, h2, h3"
    651 	 * and the $to_append selector ".some-class" the result will be
    652 	 * "h1.some-class, h2.some-class, h3.some-class".
    653 	 *
    654 	 * @since 5.8.0
    655 	 *
    656 	 * @param string $selector  Original selector.
    657 	 * @param string $to_append Selector to append.
    658 	 * @return string
    659 	 */
    660 	private static function append_to_selector( $selector, $to_append ) {
    661 		$new_selectors = array();
    662 		$selectors     = explode( ',', $selector );
    663 		foreach ( $selectors as $sel ) {
    664 			$new_selectors[] = $sel . $to_append;
    665 		}
    666 
    667 		return implode( ',', $new_selectors );
    668 	}
    669 
    670 	/**
    671 	 * Given an array of presets keyed by origin and the value key of the preset,
    672 	 * it returns an array where each key is the preset slug and each value the preset value.
    673 	 *
    674 	 * @since 5.8.0
    675 	 *
    676 	 * @param array  $preset_per_origin Array of presets keyed by origin.
    677 	 * @param string $value_key         The property of the preset that contains its value.
    678 	 * @return array Array of presets where each key is a slug and each value is the preset value.
    679 	 */
    680 	private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
    681 		$result = array();
    682 		foreach ( self::VALID_ORIGINS as $origin ) {
    683 			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
    684 				continue;
    685 			}
    686 			foreach ( $preset_per_origin[ $origin ] as $preset ) {
    687 				/*
    688 				 * We don't want to use kebabCase here,
    689 				 * see https://github.com/WordPress/gutenberg/issues/32347
    690 				 * However, we need to make sure the generated class or CSS variable
    691 				 * doesn't contain spaces.
    692 				 */
    693 				$result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];
    694 			}
    695 		}
    696 		return $result;
    697 	}
    698 
    699 	/**
    700 	 * Given a settings array, it returns the generated rulesets
    701 	 * for the preset classes.
    702 	 *
    703 	 * @since 5.8.0
    704 	 *
    705 	 * @param array  $settings Settings to process.
    706 	 * @param string $selector Selector wrapping the classes.
    707 	 * @return string The result of processing the presets.
    708 	 */
    709 	private static function compute_preset_classes( $settings, $selector ) {
    710 		if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
    711 			// Classes at the global level do not need any CSS prefixed,
    712 			// and we don't want to increase its specificity.
    713 			$selector = '';
    714 		}
    715 
    716 		$stylesheet = '';
    717 		foreach ( self::PRESETS_METADATA as $preset ) {
    718 			$preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
    719 			$preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
    720 			foreach ( $preset['classes'] as $class ) {
    721 				foreach ( $preset_by_slug as $slug => $value ) {
    722 					$stylesheet .= self::to_ruleset(
    723 						self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
    724 						array(
    725 							array(
    726 								'name'  => $class['property_name'],
    727 								'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important',
    728 							),
    729 						)
    730 					);
    731 				}
    732 			}
    733 		}
    734 
    735 		return $stylesheet;
    736 	}
    737 
    738 	/**
    739 	 * Given the block settings, it extracts the CSS Custom Properties
    740 	 * for the presets and adds them to the $declarations array
    741 	 * following the format:
    742 	 *
    743 	 *     array(
    744 	 *       'name'  => 'property_name',
    745 	 *       'value' => 'property_value,
    746 	 *     )
    747 	 *
    748 	 * @since 5.8.0
    749 	 *
    750 	 * @param array $settings Settings to process.
    751 	 * @return array Returns the modified $declarations.
    752 	 */
    753 	private static function compute_preset_vars( $settings ) {
    754 		$declarations = array();
    755 		foreach ( self::PRESETS_METADATA as $preset ) {
    756 			$preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
    757 			$preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
    758 			foreach ( $preset_by_slug as $slug => $value ) {
    759 				$declarations[] = array(
    760 					'name'  => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ),
    761 					'value' => $value,
    762 				);
    763 			}
    764 		}
    765 
    766 		return $declarations;
    767 	}
    768 
    769 	/**
    770 	 * Given an array of settings, it extracts the CSS Custom Properties
    771 	 * for the custom values and adds them to the $declarations
    772 	 * array following the format:
    773 	 *
    774 	 *     array(
    775 	 *       'name'  => 'property_name',
    776 	 *       'value' => 'property_value,
    777 	 *     )
    778 	 *
    779 	 * @since 5.8.0
    780 	 *
    781 	 * @param array $settings Settings to process.
    782 	 * @return array Returns the modified $declarations.
    783 	 */
    784 	private static function compute_theme_vars( $settings ) {
    785 		$declarations  = array();
    786 		$custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
    787 		$css_vars      = self::flatten_tree( $custom_values );
    788 		foreach ( $css_vars as $key => $value ) {
    789 			$declarations[] = array(
    790 				'name'  => '--wp--custom--' . $key,
    791 				'value' => $value,
    792 			);
    793 		}
    794 
    795 		return $declarations;
    796 	}
    797 
    798 	/**
    799 	 * Given a tree, it creates a flattened one
    800 	 * by merging the keys and binding the leaf values
    801 	 * to the new keys.
    802 	 *
    803 	 * It also transforms camelCase names into kebab-case
    804 	 * and substitutes '/' by '-'.
    805 	 *
    806 	 * This is thought to be useful to generate
    807 	 * CSS Custom Properties from a tree,
    808 	 * although there's nothing in the implementation
    809 	 * of this function that requires that format.
    810 	 *
    811 	 * For example, assuming the given prefix is '--wp'
    812 	 * and the token is '--', for this input tree:
    813 	 *
    814 	 *     {
    815 	 *       'some/property': 'value',
    816 	 *       'nestedProperty': {
    817 	 *         'sub-property': 'value'
    818 	 *       }
    819 	 *     }
    820 	 *
    821 	 * it'll return this output:
    822 	 *
    823 	 *     {
    824 	 *       '--wp--some-property': 'value',
    825 	 *       '--wp--nested-property--sub-property': 'value'
    826 	 *     }
    827 	 *
    828 	 * @since 5.8.0
    829 	 *
    830 	 * @param array  $tree   Input tree to process.
    831 	 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
    832 	 * @param string $token  Optional. Token to use between levels. Default '--'.
    833 	 * @return array The flattened tree.
    834 	 */
    835 	private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
    836 		$result = array();
    837 		foreach ( $tree as $property => $value ) {
    838 			$new_key = $prefix . str_replace(
    839 				'/',
    840 				'-',
    841 				strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
    842 			);
    843 
    844 			if ( is_array( $value ) ) {
    845 				$new_prefix = $new_key . $token;
    846 				$result     = array_merge(
    847 					$result,
    848 					self::flatten_tree( $value, $new_prefix, $token )
    849 				);
    850 			} else {
    851 				$result[ $new_key ] = $value;
    852 			}
    853 		}
    854 		return $result;
    855 	}
    856 
    857 	/**
    858 	 * Given a styles array, it extracts the style properties
    859 	 * and adds them to the $declarations array following the format:
    860 	 *
    861 	 *     array(
    862 	 *       'name'  => 'property_name',
    863 	 *       'value' => 'property_value,
    864 	 *     )
    865 	 *
    866 	 * @since 5.8.0
    867 	 *
    868 	 * @param array $styles Styles to process.
    869 	 * @return array Returns the modified $declarations.
    870 	 */
    871 	private static function compute_style_properties( $styles ) {
    872 		$declarations = array();
    873 		if ( empty( $styles ) ) {
    874 			return $declarations;
    875 		}
    876 
    877 		$properties = array();
    878 		foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
    879 			/*
    880 			 * Some properties can be shorthand properties, meaning that
    881 			 * they contain multiple values instead of a single one.
    882 			 * An example of this is the padding property.
    883 			 */
    884 			if ( self::has_properties( $metadata ) ) {
    885 				foreach ( $metadata['properties'] as $property ) {
    886 					$properties[] = array(
    887 						'name'  => $name . '-' . $property,
    888 						'value' => array_merge( $metadata['value'], array( $property ) ),
    889 					);
    890 				}
    891 			} else {
    892 				$properties[] = array(
    893 					'name'  => $name,
    894 					'value' => $metadata['value'],
    895 				);
    896 			}
    897 		}
    898 
    899 		foreach ( $properties as $prop ) {
    900 			$value = self::get_property_value( $styles, $prop['value'] );
    901 			if ( empty( $value ) ) {
    902 				continue;
    903 			}
    904 
    905 			$declarations[] = array(
    906 				'name'  => $prop['name'],
    907 				'value' => $value,
    908 			);
    909 		}
    910 
    911 		return $declarations;
    912 	}
    913 
    914 	/**
    915 	 * Whether the metadata contains a key named properties.
    916 	 *
    917 	 * @since 5.8.0
    918 	 *
    919 	 * @param array $metadata Description of the style property.
    920 	 * @return bool True if properties exists, false otherwise.
    921 	 */
    922 	private static function has_properties( $metadata ) {
    923 		if ( array_key_exists( 'properties', $metadata ) ) {
    924 			return true;
    925 		}
    926 
    927 		return false;
    928 	}
    929 
    930 	/**
    931 	 * Returns the style property for the given path.
    932 	 *
    933 	 * It also converts CSS Custom Property stored as
    934 	 * "var:preset|color|secondary" to the form
    935 	 * "--wp--preset--color--secondary".
    936 	 *
    937 	 * @since 5.8.0
    938 	 *
    939 	 * @param array $styles Styles subtree.
    940 	 * @param array $path   Which property to process.
    941 	 * @return string Style property value.
    942 	 */
    943 	private static function get_property_value( $styles, $path ) {
    944 		$value = _wp_array_get( $styles, $path, '' );
    945 
    946 		if ( '' === $value ) {
    947 			return $value;
    948 		}
    949 
    950 		$prefix     = 'var:';
    951 		$prefix_len = strlen( $prefix );
    952 		$token_in   = '|';
    953 		$token_out  = '--';
    954 		if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
    955 			$unwrapped_name = str_replace(
    956 				$token_in,
    957 				$token_out,
    958 				substr( $value, $prefix_len )
    959 			);
    960 			$value          = "var(--wp--$unwrapped_name)";
    961 		}
    962 
    963 		return $value;
    964 	}
    965 
    966 	/**
    967 	 * Builds metadata for the setting nodes, which returns in the form of:
    968 	 *
    969 	 *     [
    970 	 *       [
    971 	 *         'path'     => ['path', 'to', 'some', 'node' ],
    972 	 *         'selector' => 'CSS selector for some node'
    973 	 *       ],
    974 	 *       [
    975 	 *         'path'     => [ 'path', 'to', 'other', 'node' ],
    976 	 *         'selector' => 'CSS selector for other node'
    977 	 *       ],
    978 	 *     ]
    979 	 *
    980 	 * @since 5.8.0
    981 	 *
    982 	 * @param array $theme_json The tree to extract setting nodes from.
    983 	 * @param array $selectors  List of selectors per block.
    984 	 * @return array
    985 	 */
    986 	private static function get_setting_nodes( $theme_json, $selectors = array() ) {
    987 		$nodes = array();
    988 		if ( ! isset( $theme_json['settings'] ) ) {
    989 			return $nodes;
    990 		}
    991 
    992 		// Top-level.
    993 		$nodes[] = array(
    994 			'path'     => array( 'settings' ),
    995 			'selector' => self::ROOT_BLOCK_SELECTOR,
    996 		);
    997 
    998 		// Calculate paths for blocks.
    999 		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
   1000 			return $nodes;
   1001 		}
   1002 
   1003 		foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
   1004 			$selector = null;
   1005 			if ( isset( $selectors[ $name ]['selector'] ) ) {
   1006 				$selector = $selectors[ $name ]['selector'];
   1007 			}
   1008 
   1009 			$nodes[] = array(
   1010 				'path'     => array( 'settings', 'blocks', $name ),
   1011 				'selector' => $selector,
   1012 			);
   1013 		}
   1014 
   1015 		return $nodes;
   1016 	}
   1017 
   1018 
   1019 	/**
   1020 	 * Builds metadata for the style nodes, which returns in the form of:
   1021 	 *
   1022 	 *     [
   1023 	 *       [
   1024 	 *         'path'     => [ 'path', 'to', 'some', 'node' ],
   1025 	 *         'selector' => 'CSS selector for some node'
   1026 	 *       ],
   1027 	 *       [
   1028 	 *         'path'     => ['path', 'to', 'other', 'node' ],
   1029 	 *         'selector' => 'CSS selector for other node'
   1030 	 *       ],
   1031 	 *     ]
   1032 	 *
   1033 	 * @since 5.8.0
   1034 	 *
   1035 	 * @param array $theme_json The tree to extract style nodes from.
   1036 	 * @param array $selectors  List of selectors per block.
   1037 	 * @return array
   1038 	 */
   1039 	private static function get_style_nodes( $theme_json, $selectors = array() ) {
   1040 		$nodes = array();
   1041 		if ( ! isset( $theme_json['styles'] ) ) {
   1042 			return $nodes;
   1043 		}
   1044 
   1045 		// Top-level.
   1046 		$nodes[] = array(
   1047 			'path'     => array( 'styles' ),
   1048 			'selector' => self::ROOT_BLOCK_SELECTOR,
   1049 		);
   1050 
   1051 		if ( isset( $theme_json['styles']['elements'] ) ) {
   1052 			foreach ( $theme_json['styles']['elements'] as $element => $node ) {
   1053 				$nodes[] = array(
   1054 					'path'     => array( 'styles', 'elements', $element ),
   1055 					'selector' => self::ELEMENTS[ $element ],
   1056 				);
   1057 			}
   1058 		}
   1059 
   1060 		// Blocks.
   1061 		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
   1062 			return $nodes;
   1063 		}
   1064 
   1065 		foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
   1066 			$selector = null;
   1067 			if ( isset( $selectors[ $name ]['selector'] ) ) {
   1068 				$selector = $selectors[ $name ]['selector'];
   1069 			}
   1070 
   1071 			$nodes[] = array(
   1072 				'path'     => array( 'styles', 'blocks', $name ),
   1073 				'selector' => $selector,
   1074 			);
   1075 
   1076 			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
   1077 				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
   1078 					$nodes[] = array(
   1079 						'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
   1080 						'selector' => $selectors[ $name ]['elements'][ $element ],
   1081 					);
   1082 				}
   1083 			}
   1084 		}
   1085 
   1086 		return $nodes;
   1087 	}
   1088 
   1089 	/**
   1090 	 * Merge new incoming data.
   1091 	 *
   1092 	 * @since 5.8.0
   1093 	 *
   1094 	 * @param WP_Theme_JSON $incoming Data to merge.
   1095 	 */
   1096 	public function merge( $incoming ) {
   1097 		$incoming_data    = $incoming->get_raw_data();
   1098 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
   1099 
   1100 		/*
   1101 		 * The array_replace_recursive() algorithm merges at the leaf level.
   1102 		 * For leaf values that are arrays it will use the numeric indexes for replacement.
   1103 		 * In those cases, we want to replace the existing with the incoming value, if it exists.
   1104 		 */
   1105 		$to_replace   = array();
   1106 		$to_replace[] = array( 'spacing', 'units' );
   1107 		$to_replace[] = array( 'color', 'duotone' );
   1108 		foreach ( self::VALID_ORIGINS as $origin ) {
   1109 			$to_replace[] = array( 'color', 'palette', $origin );
   1110 			$to_replace[] = array( 'color', 'gradients', $origin );
   1111 			$to_replace[] = array( 'typography', 'fontSizes', $origin );
   1112 			$to_replace[] = array( 'typography', 'fontFamilies', $origin );
   1113 		}
   1114 
   1115 		$nodes = self::get_setting_nodes( $this->theme_json );
   1116 		foreach ( $nodes as $metadata ) {
   1117 			foreach ( $to_replace as $property_path ) {
   1118 				$path = array_merge( $metadata['path'], $property_path );
   1119 				$node = _wp_array_get( $incoming_data, $path, null );
   1120 				if ( isset( $node ) ) {
   1121 					_wp_array_set( $this->theme_json, $path, $node );
   1122 				}
   1123 			}
   1124 		}
   1125 	}
   1126 
   1127 	/**
   1128 	 * Returns the raw data.
   1129 	 *
   1130 	 * @since 5.8.0
   1131 	 *
   1132 	 * @return array Raw data.
   1133 	 */
   1134 	public function get_raw_data() {
   1135 		return $this->theme_json;
   1136 	}
   1137 
   1138 	/**
   1139 	 * Transforms the given editor settings according the
   1140 	 * add_theme_support format to the theme.json format.
   1141 	 *
   1142 	 * @since 5.8.0
   1143 	 *
   1144 	 * @param array $settings Existing editor settings.
   1145 	 * @return array Config that adheres to the theme.json schema.
   1146 	 */
   1147 	public static function get_from_editor_settings( $settings ) {
   1148 		$theme_settings = array(
   1149 			'version'  => self::LATEST_SCHEMA,
   1150 			'settings' => array(),
   1151 		);
   1152 
   1153 		// Deprecated theme supports.
   1154 		if ( isset( $settings['disableCustomColors'] ) ) {
   1155 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
   1156 				$theme_settings['settings']['color'] = array();
   1157 			}
   1158 			$theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
   1159 		}
   1160 
   1161 		if ( isset( $settings['disableCustomGradients'] ) ) {
   1162 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
   1163 				$theme_settings['settings']['color'] = array();
   1164 			}
   1165 			$theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
   1166 		}
   1167 
   1168 		if ( isset( $settings['disableCustomFontSizes'] ) ) {
   1169 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
   1170 				$theme_settings['settings']['typography'] = array();
   1171 			}
   1172 			$theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
   1173 		}
   1174 
   1175 		if ( isset( $settings['enableCustomLineHeight'] ) ) {
   1176 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
   1177 				$theme_settings['settings']['typography'] = array();
   1178 			}
   1179 			$theme_settings['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight'];
   1180 		}
   1181 
   1182 		if ( isset( $settings['enableCustomUnits'] ) ) {
   1183 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
   1184 				$theme_settings['settings']['spacing'] = array();
   1185 			}
   1186 			$theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
   1187 				array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
   1188 				$settings['enableCustomUnits'];
   1189 		}
   1190 
   1191 		if ( isset( $settings['colors'] ) ) {
   1192 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
   1193 				$theme_settings['settings']['color'] = array();
   1194 			}
   1195 			$theme_settings['settings']['color']['palette'] = $settings['colors'];
   1196 		}
   1197 
   1198 		if ( isset( $settings['gradients'] ) ) {
   1199 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
   1200 				$theme_settings['settings']['color'] = array();
   1201 			}
   1202 			$theme_settings['settings']['color']['gradients'] = $settings['gradients'];
   1203 		}
   1204 
   1205 		if ( isset( $settings['fontSizes'] ) ) {
   1206 			$font_sizes = $settings['fontSizes'];
   1207 			// Back-compatibility for presets without units.
   1208 			foreach ( $font_sizes as $key => $font_size ) {
   1209 				if ( is_numeric( $font_size['size'] ) ) {
   1210 					$font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
   1211 				}
   1212 			}
   1213 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
   1214 				$theme_settings['settings']['typography'] = array();
   1215 			}
   1216 			$theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
   1217 		}
   1218 
   1219 		if ( isset( $settings['enableCustomSpacing'] ) ) {
   1220 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
   1221 				$theme_settings['settings']['spacing'] = array();
   1222 			}
   1223 			$theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
   1224 		}
   1225 
   1226 		// Things that didn't land in core yet, so didn't have a setting assigned.
   1227 		if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) {
   1228 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
   1229 				$theme_settings['settings']['color'] = array();
   1230 			}
   1231 			$theme_settings['settings']['color']['link'] = true;
   1232 		}
   1233 
   1234 		return $theme_settings;
   1235 	}
   1236 
   1237 }