wp/wp-includes/class-wp-theme-json.php
changeset 18 be944660c56a
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
       
     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 }