[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/ -> class-wp-theme-json.php (source)

   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   * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
  14   * This is a low-level API that may need to do breaking changes. Please,
  15   * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
  16   *
  17   * @access private
  18   */
  19  class WP_Theme_JSON {
  20  
  21      /**
  22       * Container of data in theme.json format.
  23       *
  24       * @since 5.8.0
  25       * @var array
  26       */
  27      protected $theme_json = null;
  28  
  29      /**
  30       * Holds block metadata extracted from block.json
  31       * to be shared among all instances so we don't
  32       * process it twice.
  33       *
  34       * @since 5.8.0
  35       * @var array
  36       */
  37      protected static $blocks_metadata = null;
  38  
  39      /**
  40       * The CSS selector for the top-level styles.
  41       *
  42       * @since 5.8.0
  43       * @var string
  44       */
  45      const ROOT_BLOCK_SELECTOR = 'body';
  46  
  47      /**
  48       * The sources of data this object can represent.
  49       *
  50       * @since 5.8.0
  51       * @var string[]
  52       */
  53      const VALID_ORIGINS = array(
  54          'default',
  55          'theme',
  56          'custom',
  57      );
  58  
  59      /**
  60       * Presets are a set of values that serve
  61       * to bootstrap some styles: colors, font sizes, etc.
  62       *
  63       * They are a unkeyed array of values such as:
  64       *
  65       * ```php
  66       * array(
  67       *   array(
  68       *     'slug'      => 'unique-name-within-the-set',
  69       *     'name'      => 'Name for the UI',
  70       *     <value_key> => 'value'
  71       *   ),
  72       * )
  73       * ```
  74       *
  75       * This contains the necessary metadata to process them:
  76       *
  77       * - path             => Where to find the preset within the settings section.
  78       * - prevent_override => Disables override of default presets by theme presets.
  79       *                       The relationship between whether to override the defaults
  80       *                       and whether the defaults are enabled is inverse:
  81       *                         - If defaults are enabled  => theme presets should not be overriden
  82       *                         - If defaults are disabled => theme presets should be overriden
  83       *                       For example, a theme sets defaultPalette to false,
  84       *                       making the default palette hidden from the user.
  85       *                       In that case, we want all the theme presets to be present,
  86       *                       so they should override the defaults by setting this false.
  87       * - use_default_names => whether to use the default names
  88       * - value_key        => the key that represents the value
  89       * - value_func       => optionally, instead of value_key, a function to generate
  90       *                       the value that takes a preset as an argument
  91       *                       (either value_key or value_func should be present)
  92       * - css_vars         => template string to use in generating the CSS Custom Property.
  93       *                       Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
  94       *                       substituting the $slug for the slug's value for each preset value.
  95       * - classes          => array containing a structure with the classes to
  96       *                       generate for the presets, where for each array item
  97       *                       the key is the class name and the value the property name.
  98       *                       The "$slug" substring will be replaced by the slug of each preset.
  99       *                       For example:
 100       *                       'classes' => array(
 101       *                         '.has-$slug-color'            => 'color',
 102       *                         '.has-$slug-background-color' => 'background-color',
 103       *                         '.has-$slug-border-color'     => 'border-color',
 104       *                       )
 105       * - properties       => array of CSS properties to be used by kses to
 106       *                       validate the content of each preset
 107       *                       by means of the remove_insecure_properties method.
 108       *
 109       * @since 5.8.0
 110       * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
 111       *              `use_default_names` preset key, and simplified the metadata structure.
 112       * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
 113       *              `prevent_overried` value for `color.duotone` to use `color.defaultDuotone`.
 114       * @var array
 115       */
 116      const PRESETS_METADATA = array(
 117          array(
 118              'path'              => array( 'color', 'palette' ),
 119              'prevent_override'  => array( 'color', 'defaultPalette' ),
 120              'use_default_names' => false,
 121              'value_key'         => 'color',
 122              'css_vars'          => '--wp--preset--color--$slug',
 123              'classes'           => array(
 124                  '.has-$slug-color'            => 'color',
 125                  '.has-$slug-background-color' => 'background-color',
 126                  '.has-$slug-border-color'     => 'border-color',
 127              ),
 128              'properties'        => array( 'color', 'background-color', 'border-color' ),
 129          ),
 130          array(
 131              'path'              => array( 'color', 'gradients' ),
 132              'prevent_override'  => array( 'color', 'defaultGradients' ),
 133              'use_default_names' => false,
 134              'value_key'         => 'gradient',
 135              'css_vars'          => '--wp--preset--gradient--$slug',
 136              'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
 137              'properties'        => array( 'background' ),
 138          ),
 139          array(
 140              'path'              => array( 'color', 'duotone' ),
 141              'prevent_override'  => array( 'color', 'defaultDuotone' ),
 142              'use_default_names' => false,
 143              'value_func'        => 'wp_get_duotone_filter_property',
 144              'css_vars'          => '--wp--preset--duotone--$slug',
 145              'classes'           => array(),
 146              'properties'        => array( 'filter' ),
 147          ),
 148          array(
 149              'path'              => array( 'typography', 'fontSizes' ),
 150              'prevent_override'  => false,
 151              'use_default_names' => true,
 152              'value_key'         => 'size',
 153              'css_vars'          => '--wp--preset--font-size--$slug',
 154              'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
 155              'properties'        => array( 'font-size' ),
 156          ),
 157          array(
 158              'path'              => array( 'typography', 'fontFamilies' ),
 159              'prevent_override'  => false,
 160              'use_default_names' => false,
 161              'value_key'         => 'fontFamily',
 162              'css_vars'          => '--wp--preset--font-family--$slug',
 163              'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
 164              'properties'        => array( 'font-family' ),
 165          ),
 166      );
 167  
 168      /**
 169       * Metadata for style properties.
 170       *
 171       * Each element is a direct mapping from the CSS property name to the
 172       * path to the value in theme.json & block attributes.
 173       *
 174       * @since 5.8.0
 175       * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
 176       *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
 177       *              `text-decoration`, `text-transform`, and `filter` properties,
 178       *              simplified the metadata structure.
 179       * @var array
 180       */
 181      const PROPERTIES_METADATA = array(
 182          'background'                 => array( 'color', 'gradient' ),
 183          'background-color'           => array( 'color', 'background' ),
 184          'border-radius'              => array( 'border', 'radius' ),
 185          'border-top-left-radius'     => array( 'border', 'radius', 'topLeft' ),
 186          'border-top-right-radius'    => array( 'border', 'radius', 'topRight' ),
 187          'border-bottom-left-radius'  => array( 'border', 'radius', 'bottomLeft' ),
 188          'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
 189          'border-color'               => array( 'border', 'color' ),
 190          'border-width'               => array( 'border', 'width' ),
 191          'border-style'               => array( 'border', 'style' ),
 192          'color'                      => array( 'color', 'text' ),
 193          'font-family'                => array( 'typography', 'fontFamily' ),
 194          'font-size'                  => array( 'typography', 'fontSize' ),
 195          'font-style'                 => array( 'typography', 'fontStyle' ),
 196          'font-weight'                => array( 'typography', 'fontWeight' ),
 197          'letter-spacing'             => array( 'typography', 'letterSpacing' ),
 198          'line-height'                => array( 'typography', 'lineHeight' ),
 199          'margin'                     => array( 'spacing', 'margin' ),
 200          'margin-top'                 => array( 'spacing', 'margin', 'top' ),
 201          'margin-right'               => array( 'spacing', 'margin', 'right' ),
 202          'margin-bottom'              => array( 'spacing', 'margin', 'bottom' ),
 203          'margin-left'                => array( 'spacing', 'margin', 'left' ),
 204          'padding'                    => array( 'spacing', 'padding' ),
 205          'padding-top'                => array( 'spacing', 'padding', 'top' ),
 206          'padding-right'              => array( 'spacing', 'padding', 'right' ),
 207          'padding-bottom'             => array( 'spacing', 'padding', 'bottom' ),
 208          'padding-left'               => array( 'spacing', 'padding', 'left' ),
 209          '--wp--style--block-gap'     => array( 'spacing', 'blockGap' ),
 210          'text-decoration'            => array( 'typography', 'textDecoration' ),
 211          'text-transform'             => array( 'typography', 'textTransform' ),
 212          'filter'                     => array( 'filter', 'duotone' ),
 213      );
 214  
 215      /**
 216       * Protected style properties.
 217       *
 218       * These style properties are only rendered if a setting enables it
 219       * via a value other than `null`.
 220       *
 221       * Each element maps the style property to the corresponding theme.json
 222       * setting key.
 223       *
 224       * @since 5.9.0
 225       */
 226      const PROTECTED_PROPERTIES = array(
 227          'spacing.blockGap' => array( 'spacing', 'blockGap' ),
 228      );
 229  
 230      /**
 231       * The top-level keys a theme.json can have.
 232       *
 233       * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
 234       * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
 235       *              added the `customTemplates` and `templateParts` values.
 236       * @var string[]
 237       */
 238      const VALID_TOP_LEVEL_KEYS = array(
 239          'customTemplates',
 240          'patterns',
 241          'settings',
 242          'styles',
 243          'templateParts',
 244          'version',
 245          'title',
 246      );
 247  
 248      /**
 249       * The valid properties under the settings key.
 250       *
 251       * @since 5.8.0 As `ALLOWED_SETTINGS`.
 252       * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
 253       *              added new properties for `border`, `color`, `spacing`,
 254       *              and `typography`, and renamed others according to the new schema.
 255       * @since 6.0.0 Added `color.defaultDuotone`.
 256       * @var array
 257       */
 258      const VALID_SETTINGS = array(
 259          'appearanceTools' => null,
 260          'border'          => array(
 261              'color'  => null,
 262              'radius' => null,
 263              'style'  => null,
 264              'width'  => null,
 265          ),
 266          'color'           => array(
 267              'background'       => null,
 268              'custom'           => null,
 269              'customDuotone'    => null,
 270              'customGradient'   => null,
 271              'defaultDuotone'   => null,
 272              'defaultGradients' => null,
 273              'defaultPalette'   => null,
 274              'duotone'          => null,
 275              'gradients'        => null,
 276              'link'             => null,
 277              'palette'          => null,
 278              'text'             => null,
 279          ),
 280          'custom'          => null,
 281          'layout'          => array(
 282              'contentSize' => null,
 283              'wideSize'    => null,
 284          ),
 285          'spacing'         => array(
 286              'blockGap' => null,
 287              'margin'   => null,
 288              'padding'  => null,
 289              'units'    => null,
 290          ),
 291          'typography'      => array(
 292              'customFontSize' => null,
 293              'dropCap'        => null,
 294              'fontFamilies'   => null,
 295              'fontSizes'      => null,
 296              'fontStyle'      => null,
 297              'fontWeight'     => null,
 298              'letterSpacing'  => null,
 299              'lineHeight'     => null,
 300              'textDecoration' => null,
 301              'textTransform'  => null,
 302          ),
 303      );
 304  
 305      /**
 306       * The valid properties under the styles key.
 307       *
 308       * @since 5.8.0 As `ALLOWED_STYLES`.
 309       * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
 310       *              added new properties for `border`, `filter`, `spacing`,
 311       *              and `typography`.
 312       * @var array
 313       */
 314      const VALID_STYLES = array(
 315          'border'     => array(
 316              'color'  => null,
 317              'radius' => null,
 318              'style'  => null,
 319              'width'  => null,
 320          ),
 321          'color'      => array(
 322              'background' => null,
 323              'gradient'   => null,
 324              'text'       => null,
 325          ),
 326          'filter'     => array(
 327              'duotone' => null,
 328          ),
 329          'spacing'    => array(
 330              'margin'   => null,
 331              'padding'  => null,
 332              'blockGap' => 'top',
 333          ),
 334          'typography' => array(
 335              'fontFamily'     => null,
 336              'fontSize'       => null,
 337              'fontStyle'      => null,
 338              'fontWeight'     => null,
 339              'letterSpacing'  => null,
 340              'lineHeight'     => null,
 341              'textDecoration' => null,
 342              'textTransform'  => null,
 343          ),
 344      );
 345  
 346      /**
 347       * The valid elements that can be found under styles.
 348       *
 349       * @since 5.8.0
 350       * @var string[]
 351       */
 352      const ELEMENTS = array(
 353          'link' => 'a',
 354          'h1'   => 'h1',
 355          'h2'   => 'h2',
 356          'h3'   => 'h3',
 357          'h4'   => 'h4',
 358          'h5'   => 'h5',
 359          'h6'   => 'h6',
 360      );
 361  
 362      /**
 363       * Options that settings.appearanceTools enables.
 364       *
 365       * @since 6.0.0
 366       * @var array
 367       */
 368      const APPEARANCE_TOOLS_OPT_INS = array(
 369          array( 'border', 'color' ),
 370          array( 'border', 'radius' ),
 371          array( 'border', 'style' ),
 372          array( 'border', 'width' ),
 373          array( 'color', 'link' ),
 374          array( 'spacing', 'blockGap' ),
 375          array( 'spacing', 'margin' ),
 376          array( 'spacing', 'padding' ),
 377          array( 'typography', 'lineHeight' ),
 378      );
 379  
 380      /**
 381       * The latest version of the schema in use.
 382       *
 383       * @since 5.8.0
 384       * @since 5.9.0 Changed value from 1 to 2.
 385       * @var int
 386       */
 387      const LATEST_SCHEMA = 2;
 388  
 389      /**
 390       * Constructor.
 391       *
 392       * @since 5.8.0
 393       *
 394       * @param array  $theme_json A structure that follows the theme.json schema.
 395       * @param string $origin     Optional. What source of data this object represents.
 396       *                           One of 'default', 'theme', or 'custom'. Default 'theme'.
 397       */
 398  	public function __construct( $theme_json = array(), $origin = 'theme' ) {
 399          if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
 400              $origin = 'theme';
 401          }
 402  
 403          $this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json );
 404          $valid_block_names   = array_keys( static::get_blocks_metadata() );
 405          $valid_element_names = array_keys( static::ELEMENTS );
 406          $theme_json          = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
 407          $this->theme_json    = static::maybe_opt_in_into_settings( $theme_json );
 408  
 409          // Internally, presets are keyed by origin.
 410          $nodes = static::get_setting_nodes( $this->theme_json );
 411          foreach ( $nodes as $node ) {
 412              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
 413                  $path   = array_merge( $node['path'], $preset_metadata['path'] );
 414                  $preset = _wp_array_get( $this->theme_json, $path, null );
 415                  if ( null !== $preset ) {
 416                      // If the preset is not already keyed by origin.
 417                      if ( isset( $preset[0] ) || empty( $preset ) ) {
 418                          _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
 419                      }
 420                  }
 421              }
 422          }
 423      }
 424  
 425      /**
 426       * Enables some opt-in settings if theme declared support.
 427       *
 428       * @since 5.9.0
 429       *
 430       * @param array $theme_json A theme.json structure to modify.
 431       * @return array The modified theme.json structure.
 432       */
 433  	protected static function maybe_opt_in_into_settings( $theme_json ) {
 434          $new_theme_json = $theme_json;
 435  
 436          if (
 437              isset( $new_theme_json['settings']['appearanceTools'] ) &&
 438              true === $new_theme_json['settings']['appearanceTools']
 439          ) {
 440              static::do_opt_in_into_settings( $new_theme_json['settings'] );
 441          }
 442  
 443          if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
 444              foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
 445                  if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
 446                      static::do_opt_in_into_settings( $block );
 447                  }
 448              }
 449          }
 450  
 451          return $new_theme_json;
 452      }
 453  
 454      /**
 455       * Enables some settings.
 456       *
 457       * @since 5.9.0
 458       *
 459       * @param array $context The context to which the settings belong.
 460       */
 461  	protected static function do_opt_in_into_settings( &$context ) {
 462          foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
 463              // Use "unset prop" as a marker instead of "null" because
 464              // "null" can be a valid value for some props (e.g. blockGap).
 465              if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
 466                  _wp_array_set( $context, $path, true );
 467              }
 468          }
 469  
 470          unset( $context['appearanceTools'] );
 471      }
 472  
 473      /**
 474       * Sanitizes the input according to the schemas.
 475       *
 476       * @since 5.8.0
 477       * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
 478       *
 479       * @param array $input               Structure to sanitize.
 480       * @param array $valid_block_names   List of valid block names.
 481       * @param array $valid_element_names List of valid element names.
 482       * @return array The sanitized output.
 483       */
 484  	protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
 485          $output = array();
 486  
 487          if ( ! is_array( $input ) ) {
 488              return $output;
 489          }
 490  
 491          $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
 492  
 493          // Some styles are only meant to be available at the top-level (e.g.: blockGap),
 494          // hence, the schema for blocks & elements should not have them.
 495          $styles_non_top_level = static::VALID_STYLES;
 496          foreach ( array_keys( $styles_non_top_level ) as $section ) {
 497              foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
 498                  if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
 499                      unset( $styles_non_top_level[ $section ][ $prop ] );
 500                  }
 501              }
 502          }
 503  
 504          // Build the schema based on valid block & element names.
 505          $schema                 = array();
 506          $schema_styles_elements = array();
 507          foreach ( $valid_element_names as $element ) {
 508              $schema_styles_elements[ $element ] = $styles_non_top_level;
 509          }
 510          $schema_styles_blocks   = array();
 511          $schema_settings_blocks = array();
 512          foreach ( $valid_block_names as $block ) {
 513              $schema_settings_blocks[ $block ]           = static::VALID_SETTINGS;
 514              $schema_styles_blocks[ $block ]             = $styles_non_top_level;
 515              $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
 516          }
 517          $schema['styles']             = static::VALID_STYLES;
 518          $schema['styles']['blocks']   = $schema_styles_blocks;
 519          $schema['styles']['elements'] = $schema_styles_elements;
 520          $schema['settings']           = static::VALID_SETTINGS;
 521          $schema['settings']['blocks'] = $schema_settings_blocks;
 522  
 523          // Remove anything that's not present in the schema.
 524          foreach ( array( 'styles', 'settings' ) as $subtree ) {
 525              if ( ! isset( $input[ $subtree ] ) ) {
 526                  continue;
 527              }
 528  
 529              if ( ! is_array( $input[ $subtree ] ) ) {
 530                  unset( $output[ $subtree ] );
 531                  continue;
 532              }
 533  
 534              $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
 535  
 536              if ( empty( $result ) ) {
 537                  unset( $output[ $subtree ] );
 538              } else {
 539                  $output[ $subtree ] = $result;
 540              }
 541          }
 542  
 543          return $output;
 544      }
 545  
 546      /**
 547       * Returns the metadata for each block.
 548       *
 549       * Example:
 550       *
 551       *     {
 552       *       'core/paragraph': {
 553       *         'selector': 'p',
 554       *         'elements': {
 555       *           'link' => 'link selector',
 556       *           'etc'  => 'element selector'
 557       *         }
 558       *       },
 559       *       'core/heading': {
 560       *         'selector': 'h1',
 561       *         'elements': {}
 562       *       },
 563       *       'core/image': {
 564       *         'selector': '.wp-block-image',
 565       *         'duotone': 'img',
 566       *         'elements': {}
 567       *       }
 568       *     }
 569       *
 570       * @since 5.8.0
 571       * @since 5.9.0 Added `duotone` key with CSS selector.
 572       *
 573       * @return array Block metadata.
 574       */
 575  	protected static function get_blocks_metadata() {
 576          if ( null !== static::$blocks_metadata ) {
 577              return static::$blocks_metadata;
 578          }
 579  
 580          static::$blocks_metadata = array();
 581  
 582          $registry = WP_Block_Type_Registry::get_instance();
 583          $blocks   = $registry->get_all_registered();
 584          foreach ( $blocks as $block_name => $block_type ) {
 585              if (
 586                  isset( $block_type->supports['__experimentalSelector'] ) &&
 587                  is_string( $block_type->supports['__experimentalSelector'] )
 588              ) {
 589                  static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
 590              } else {
 591                  static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
 592              }
 593  
 594              if (
 595                  isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
 596                  is_string( $block_type->supports['color']['__experimentalDuotone'] )
 597              ) {
 598                  static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
 599              }
 600  
 601              // Assign defaults, then overwrite those that the block sets by itself.
 602              // If the block selector is compounded, will append the element to each
 603              // individual block selector.
 604              $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] );
 605              foreach ( static::ELEMENTS as $el_name => $el_selector ) {
 606                  $element_selector = array();
 607                  foreach ( $block_selectors as $selector ) {
 608                      $element_selector[] = $selector . ' ' . $el_selector;
 609                  }
 610                  static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
 611              }
 612          }
 613  
 614          return static::$blocks_metadata;
 615      }
 616  
 617      /**
 618       * Given a tree, removes the keys that are not present in the schema.
 619       *
 620       * It is recursive and modifies the input in-place.
 621       *
 622       * @since 5.8.0
 623       *
 624       * @param array $tree   Input to process.
 625       * @param array $schema Schema to adhere to.
 626       * @return array Returns the modified $tree.
 627       */
 628  	protected static function remove_keys_not_in_schema( $tree, $schema ) {
 629          $tree = array_intersect_key( $tree, $schema );
 630  
 631          foreach ( $schema as $key => $data ) {
 632              if ( ! isset( $tree[ $key ] ) ) {
 633                  continue;
 634              }
 635  
 636              if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
 637                  $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
 638  
 639                  if ( empty( $tree[ $key ] ) ) {
 640                      unset( $tree[ $key ] );
 641                  }
 642              } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
 643                  unset( $tree[ $key ] );
 644              }
 645          }
 646  
 647          return $tree;
 648      }
 649  
 650      /**
 651       * Returns the existing settings for each block.
 652       *
 653       * Example:
 654       *
 655       *     {
 656       *       'root': {
 657       *         'color': {
 658       *           'custom': true
 659       *         }
 660       *       },
 661       *       'core/paragraph': {
 662       *         'spacing': {
 663       *           'customPadding': true
 664       *         }
 665       *       }
 666       *     }
 667       *
 668       * @since 5.8.0
 669       *
 670       * @return array Settings per block.
 671       */
 672  	public function get_settings() {
 673          if ( ! isset( $this->theme_json['settings'] ) ) {
 674              return array();
 675          } else {
 676              return $this->theme_json['settings'];
 677          }
 678      }
 679  
 680      /**
 681       * Returns the stylesheet that results of processing
 682       * the theme.json structure this object represents.
 683       *
 684       * @since 5.8.0
 685       * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
 686       *
 687       * @param array $types   Types of styles to load. Will load all by default. It accepts:
 688       *                       - `variables`: only the CSS Custom Properties for presets & custom ones.
 689       *                       - `styles`: only the styles section in theme.json.
 690       *                       - `presets`: only the classes for the presets.
 691       * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
 692       * @return string Stylesheet.
 693       */
 694  	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
 695          if ( null === $origins ) {
 696              $origins = static::VALID_ORIGINS;
 697          }
 698  
 699          if ( is_string( $types ) ) {
 700              // Dispatch error and map old arguments to new ones.
 701              _deprecated_argument( __FUNCTION__, '5.9.0' );
 702              if ( 'block_styles' === $types ) {
 703                  $types = array( 'styles', 'presets' );
 704              } elseif ( 'css_variables' === $types ) {
 705                  $types = array( 'variables' );
 706              } else {
 707                  $types = array( 'variables', 'styles', 'presets' );
 708              }
 709          }
 710  
 711          $blocks_metadata = static::get_blocks_metadata();
 712          $style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata );
 713          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
 714  
 715          $stylesheet = '';
 716  
 717          if ( in_array( 'variables', $types, true ) ) {
 718              $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
 719          }
 720  
 721          if ( in_array( 'styles', $types, true ) ) {
 722              $stylesheet .= $this->get_block_classes( $style_nodes );
 723          }
 724  
 725          if ( in_array( 'presets', $types, true ) ) {
 726              $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
 727          }
 728  
 729          return $stylesheet;
 730      }
 731  
 732      /**
 733       * Returns the page templates of the active theme.
 734       *
 735       * @since 5.9.0
 736       *
 737       * @return array
 738       */
 739  	public function get_custom_templates() {
 740          $custom_templates = array();
 741          if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
 742              return $custom_templates;
 743          }
 744  
 745          foreach ( $this->theme_json['customTemplates'] as $item ) {
 746              if ( isset( $item['name'] ) ) {
 747                  $custom_templates[ $item['name'] ] = array(
 748                      'title'     => isset( $item['title'] ) ? $item['title'] : '',
 749                      'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
 750                  );
 751              }
 752          }
 753          return $custom_templates;
 754      }
 755  
 756      /**
 757       * Returns the template part data of active theme.
 758       *
 759       * @since 5.9.0
 760       *
 761       * @return array
 762       */
 763  	public function get_template_parts() {
 764          $template_parts = array();
 765          if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
 766              return $template_parts;
 767          }
 768  
 769          foreach ( $this->theme_json['templateParts'] as $item ) {
 770              if ( isset( $item['name'] ) ) {
 771                  $template_parts[ $item['name'] ] = array(
 772                      'title' => isset( $item['title'] ) ? $item['title'] : '',
 773                      'area'  => isset( $item['area'] ) ? $item['area'] : '',
 774                  );
 775              }
 776          }
 777          return $template_parts;
 778      }
 779  
 780      /**
 781       * Converts each style section into a list of rulesets
 782       * containing the block styles to be appended to the stylesheet.
 783       *
 784       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
 785       *
 786       * For each section this creates a new ruleset such as:
 787       *
 788       *   block-selector {
 789       *     style-property-one: value;
 790       *   }
 791       *
 792       * @since 5.8.0 As `get_block_styles()`.
 793       * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
 794       *              and no longer returns preset classes.
 795       *              Removed the `$setting_nodes` parameter.
 796       *
 797       * @param array $style_nodes Nodes with styles.
 798       * @return string The new stylesheet.
 799       */
 800  	protected function get_block_classes( $style_nodes ) {
 801          $block_rules = '';
 802  
 803          foreach ( $style_nodes as $metadata ) {
 804              if ( null === $metadata['selector'] ) {
 805                  continue;
 806              }
 807  
 808              $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
 809              $selector     = $metadata['selector'];
 810              $settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
 811              $declarations = static::compute_style_properties( $node, $settings );
 812  
 813              // 1. Separate the ones who use the general selector
 814              // and the ones who use the duotone selector.
 815              $declarations_duotone = array();
 816              foreach ( $declarations as $index => $declaration ) {
 817                  if ( 'filter' === $declaration['name'] ) {
 818                      unset( $declarations[ $index ] );
 819                      $declarations_duotone[] = $declaration;
 820                  }
 821              }
 822  
 823              /*
 824               * Reset default browser margin on the root body element.
 825               * This is set on the root selector **before** generating the ruleset
 826               * from the `theme.json`. This is to ensure that if the `theme.json` declares
 827               * `margin` in its `spacing` declaration for the `body` element then these
 828               * user-generated values take precedence in the CSS cascade.
 829               * @link https://github.com/WordPress/gutenberg/issues/36147.
 830               */
 831              if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
 832                  $block_rules .= 'body { margin: 0; }';
 833              }
 834  
 835              // 2. Generate the rules that use the general selector.
 836              $block_rules .= static::to_ruleset( $selector, $declarations );
 837  
 838              // 3. Generate the rules that use the duotone selector.
 839              if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
 840                  $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
 841                  $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
 842              }
 843  
 844              if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
 845                  $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
 846                  $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
 847                  $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
 848  
 849                  $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
 850                  if ( $has_block_gap_support ) {
 851                      $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
 852                      $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
 853                  }
 854              }
 855          }
 856  
 857          return $block_rules;
 858      }
 859  
 860      /**
 861       * Creates new rulesets as classes for each preset value such as:
 862       *
 863       *   .has-value-color {
 864       *     color: value;
 865       *   }
 866       *
 867       *   .has-value-background-color {
 868       *     background-color: value;
 869       *   }
 870       *
 871       *   .has-value-font-size {
 872       *     font-size: value;
 873       *   }
 874       *
 875       *   .has-value-gradient-background {
 876       *     background: value;
 877       *   }
 878       *
 879       *   p.has-value-gradient-background {
 880       *     background: value;
 881       *   }
 882       *
 883       * @since 5.9.0
 884       *
 885       * @param array $setting_nodes Nodes with settings.
 886       * @param array $origins       List of origins to process presets from.
 887       * @return string The new stylesheet.
 888       */
 889  	protected function get_preset_classes( $setting_nodes, $origins ) {
 890          $preset_rules = '';
 891  
 892          foreach ( $setting_nodes as $metadata ) {
 893              if ( null === $metadata['selector'] ) {
 894                  continue;
 895              }
 896  
 897              $selector      = $metadata['selector'];
 898              $node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
 899              $preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
 900          }
 901  
 902          return $preset_rules;
 903      }
 904  
 905      /**
 906       * Converts each styles section into a list of rulesets
 907       * to be appended to the stylesheet.
 908       * These rulesets contain all the css variables (custom variables and preset variables).
 909       *
 910       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
 911       *
 912       * For each section this creates a new ruleset such as:
 913       *
 914       *     block-selector {
 915       *       --wp--preset--category--slug: value;
 916       *       --wp--custom--variable: value;
 917       *     }
 918       *
 919       * @since 5.8.0
 920       * @since 5.9.0 Added the `$origins` parameter.
 921       *
 922       * @param array $nodes   Nodes with settings.
 923       * @param array $origins List of origins to process.
 924       * @return string The new stylesheet.
 925       */
 926  	protected function get_css_variables( $nodes, $origins ) {
 927          $stylesheet = '';
 928          foreach ( $nodes as $metadata ) {
 929              if ( null === $metadata['selector'] ) {
 930                  continue;
 931              }
 932  
 933              $selector = $metadata['selector'];
 934  
 935              $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
 936              $declarations = array_merge( static::compute_preset_vars( $node, $origins ), static::compute_theme_vars( $node ) );
 937  
 938              $stylesheet .= static::to_ruleset( $selector, $declarations );
 939          }
 940  
 941          return $stylesheet;
 942      }
 943  
 944      /**
 945       * Given a selector and a declaration list,
 946       * creates the corresponding ruleset.
 947       *
 948       * @since 5.8.0
 949       *
 950       * @param string $selector     CSS selector.
 951       * @param array  $declarations List of declarations.
 952       * @return string CSS ruleset.
 953       */
 954  	protected static function to_ruleset( $selector, $declarations ) {
 955          if ( empty( $declarations ) ) {
 956              return '';
 957          }
 958  
 959          $declaration_block = array_reduce(
 960              $declarations,
 961              static function ( $carry, $element ) {
 962                  return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
 963              ''
 964          );
 965  
 966          return $selector . '{' . $declaration_block . '}';
 967      }
 968  
 969      /**
 970       * Function that appends a sub-selector to a existing one.
 971       *
 972       * Given the compounded $selector "h1, h2, h3"
 973       * and the $to_append selector ".some-class" the result will be
 974       * "h1.some-class, h2.some-class, h3.some-class".
 975       *
 976       * @since 5.8.0
 977       *
 978       * @param string $selector  Original selector.
 979       * @param string $to_append Selector to append.
 980       * @return string
 981       */
 982  	protected static function append_to_selector( $selector, $to_append ) {
 983          $new_selectors = array();
 984          $selectors     = explode( ',', $selector );
 985          foreach ( $selectors as $sel ) {
 986              $new_selectors[] = $sel . $to_append;
 987          }
 988  
 989          return implode( ',', $new_selectors );
 990      }
 991  
 992      /**
 993       * Given a settings array, it returns the generated rulesets
 994       * for the preset classes.
 995       *
 996       * @since 5.8.0
 997       * @since 5.9.0 Added the `$origins` parameter.
 998       *
 999       * @param array  $settings Settings to process.
1000       * @param string $selector Selector wrapping the classes.
1001       * @param array  $origins  List of origins to process.
1002       * @return string The result of processing the presets.
1003       */
1004  	protected static function compute_preset_classes( $settings, $selector, $origins ) {
1005          if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
1006              // Classes at the global level do not need any CSS prefixed,
1007              // and we don't want to increase its specificity.
1008              $selector = '';
1009          }
1010  
1011          $stylesheet = '';
1012          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1013              $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
1014              foreach ( $preset_metadata['classes'] as $class => $property ) {
1015                  foreach ( $slugs as $slug ) {
1016                      $css_var     = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
1017                      $class_name  = static::replace_slug_in_string( $class, $slug );
1018                      $stylesheet .= static::to_ruleset(
1019                          static::append_to_selector( $selector, $class_name ),
1020                          array(
1021                              array(
1022                                  'name'  => $property,
1023                                  'value' => 'var(' . $css_var . ') !important',
1024                              ),
1025                          )
1026                      );
1027                  }
1028              }
1029          }
1030  
1031          return $stylesheet;
1032      }
1033  
1034      /**
1035       * Function that scopes a selector with another one. This works a bit like
1036       * SCSS nesting except the `&` operator isn't supported.
1037       *
1038       * <code>
1039       * $scope = '.a, .b .c';
1040       * $selector = '> .x, .y';
1041       * $merged = scope_selector( $scope, $selector );
1042       * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
1043       * </code>
1044       *
1045       * @since 5.9.0
1046       *
1047       * @param string $scope    Selector to scope to.
1048       * @param string $selector Original selector.
1049       * @return string Scoped selector.
1050       */
1051  	protected static function scope_selector( $scope, $selector ) {
1052          $scopes    = explode( ',', $scope );
1053          $selectors = explode( ',', $selector );
1054  
1055          $selectors_scoped = array();
1056          foreach ( $scopes as $outer ) {
1057              foreach ( $selectors as $inner ) {
1058                  $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner );
1059              }
1060          }
1061  
1062          return implode( ', ', $selectors_scoped );
1063      }
1064  
1065      /**
1066       * Gets preset values keyed by slugs based on settings and metadata.
1067       *
1068       * <code>
1069       * $settings = array(
1070       *     'typography' => array(
1071       *         'fontFamilies' => array(
1072       *             array(
1073       *                 'slug'       => 'sansSerif',
1074       *                 'fontFamily' => '"Helvetica Neue", sans-serif',
1075       *             ),
1076       *             array(
1077       *                 'slug'   => 'serif',
1078       *                 'colors' => 'Georgia, serif',
1079       *             )
1080       *         ),
1081       *     ),
1082       * );
1083       * $meta = array(
1084       *    'path'      => array( 'typography', 'fontFamilies' ),
1085       *    'value_key' => 'fontFamily',
1086       * );
1087       * $values_by_slug = get_settings_values_by_slug();
1088       * // $values_by_slug === array(
1089       * //   'sans-serif' => '"Helvetica Neue", sans-serif',
1090       * //   'serif'      => 'Georgia, serif',
1091       * // );
1092       * </code>
1093       *
1094       * @since 5.9.0
1095       *
1096       * @param array $settings        Settings to process.
1097       * @param array $preset_metadata One of the PRESETS_METADATA values.
1098       * @param array $origins         List of origins to process.
1099       * @return array Array of presets where each key is a slug and each value is the preset value.
1100       */
1101  	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
1102          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1103  
1104          $result = array();
1105          foreach ( $origins as $origin ) {
1106              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1107                  continue;
1108              }
1109              foreach ( $preset_per_origin[ $origin ] as $preset ) {
1110                  $slug = _wp_to_kebab_case( $preset['slug'] );
1111  
1112                  $value = '';
1113                  if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
1114                      $value_key = $preset_metadata['value_key'];
1115                      $value     = $preset[ $value_key ];
1116                  } elseif (
1117                      isset( $preset_metadata['value_func'] ) &&
1118                      is_callable( $preset_metadata['value_func'] )
1119                  ) {
1120                      $value_func = $preset_metadata['value_func'];
1121                      $value      = call_user_func( $value_func, $preset );
1122                  } else {
1123                      // If we don't have a value, then don't add it to the result.
1124                      continue;
1125                  }
1126  
1127                  $result[ $slug ] = $value;
1128              }
1129          }
1130          return $result;
1131      }
1132  
1133      /**
1134       * Similar to get_settings_values_by_slug, but doesn't compute the value.
1135       *
1136       * @since 5.9.0
1137       *
1138       * @param array $settings        Settings to process.
1139       * @param array $preset_metadata One of the PRESETS_METADATA values.
1140       * @param array $origins         List of origins to process.
1141       * @return array Array of presets where the key and value are both the slug.
1142       */
1143  	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
1144          if ( null === $origins ) {
1145              $origins = static::VALID_ORIGINS;
1146          }
1147  
1148          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1149  
1150          $result = array();
1151          foreach ( $origins as $origin ) {
1152              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1153                  continue;
1154              }
1155              foreach ( $preset_per_origin[ $origin ] as $preset ) {
1156                  $slug = _wp_to_kebab_case( $preset['slug'] );
1157  
1158                  // Use the array as a set so we don't get duplicates.
1159                  $result[ $slug ] = $slug;
1160              }
1161          }
1162          return $result;
1163      }
1164  
1165      /**
1166       * Transform a slug into a CSS Custom Property.
1167       *
1168       * @since 5.9.0
1169       *
1170       * @param string $input String to replace.
1171       * @param string $slug  The slug value to use to generate the custom property.
1172       * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
1173       */
1174  	protected static function replace_slug_in_string( $input, $slug ) {
1175          return strtr( $input, array( '$slug' => $slug ) );
1176      }
1177  
1178      /**
1179       * Given the block settings, it extracts the CSS Custom Properties
1180       * for the presets and adds them to the $declarations array
1181       * following the format:
1182       *
1183       *     array(
1184       *       'name'  => 'property_name',
1185       *       'value' => 'property_value,
1186       *     )
1187       *
1188       * @since 5.8.0
1189       * @since 5.9.0 Added the `$origins` parameter.
1190       *
1191       * @param array $settings Settings to process.
1192       * @param array $origins  List of origins to process.
1193       * @return array Returns the modified $declarations.
1194       */
1195  	protected static function compute_preset_vars( $settings, $origins ) {
1196          $declarations = array();
1197          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1198              $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
1199              foreach ( $values_by_slug as $slug => $value ) {
1200                  $declarations[] = array(
1201                      'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
1202                      'value' => $value,
1203                  );
1204              }
1205          }
1206  
1207          return $declarations;
1208      }
1209  
1210      /**
1211       * Given an array of settings, it extracts the CSS Custom Properties
1212       * for the custom values and adds them to the $declarations
1213       * array following the format:
1214       *
1215       *     array(
1216       *       'name'  => 'property_name',
1217       *       'value' => 'property_value,
1218       *     )
1219       *
1220       * @since 5.8.0
1221       *
1222       * @param array $settings Settings to process.
1223       * @return array Returns the modified $declarations.
1224       */
1225  	protected static function compute_theme_vars( $settings ) {
1226          $declarations  = array();
1227          $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
1228          $css_vars      = static::flatten_tree( $custom_values );
1229          foreach ( $css_vars as $key => $value ) {
1230              $declarations[] = array(
1231                  'name'  => '--wp--custom--' . $key,
1232                  'value' => $value,
1233              );
1234          }
1235  
1236          return $declarations;
1237      }
1238  
1239      /**
1240       * Given a tree, it creates a flattened one
1241       * by merging the keys and binding the leaf values
1242       * to the new keys.
1243       *
1244       * It also transforms camelCase names into kebab-case
1245       * and substitutes '/' by '-'.
1246       *
1247       * This is thought to be useful to generate
1248       * CSS Custom Properties from a tree,
1249       * although there's nothing in the implementation
1250       * of this function that requires that format.
1251       *
1252       * For example, assuming the given prefix is '--wp'
1253       * and the token is '--', for this input tree:
1254       *
1255       *     {
1256       *       'some/property': 'value',
1257       *       'nestedProperty': {
1258       *         'sub-property': 'value'
1259       *       }
1260       *     }
1261       *
1262       * it'll return this output:
1263       *
1264       *     {
1265       *       '--wp--some-property': 'value',
1266       *       '--wp--nested-property--sub-property': 'value'
1267       *     }
1268       *
1269       * @since 5.8.0
1270       *
1271       * @param array  $tree   Input tree to process.
1272       * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
1273       * @param string $token  Optional. Token to use between levels. Default '--'.
1274       * @return array The flattened tree.
1275       */
1276  	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
1277          $result = array();
1278          foreach ( $tree as $property => $value ) {
1279              $new_key = $prefix . str_replace(
1280                  '/',
1281                  '-',
1282                  strtolower( _wp_to_kebab_case( $property ) )
1283              );
1284  
1285              if ( is_array( $value ) ) {
1286                  $new_prefix = $new_key . $token;
1287                  $result     = array_merge(
1288                      $result,
1289                      static::flatten_tree( $value, $new_prefix, $token )
1290                  );
1291              } else {
1292                  $result[ $new_key ] = $value;
1293              }
1294          }
1295          return $result;
1296      }
1297  
1298      /**
1299       * Given a styles array, it extracts the style properties
1300       * and adds them to the $declarations array following the format:
1301       *
1302       *     array(
1303       *       'name'  => 'property_name',
1304       *       'value' => 'property_value,
1305       *     )
1306       *
1307       * @since 5.8.0
1308       * @since 5.9.0 Added the `$settings` and `$properties` parameters.
1309       *
1310       * @param array $styles    Styles to process.
1311       * @param array $settings  Theme settings.
1312       * @param array $properties Properties metadata.
1313       * @return array Returns the modified $declarations.
1314       */
1315  	protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
1316          if ( null === $properties ) {
1317              $properties = static::PROPERTIES_METADATA;
1318          }
1319  
1320          $declarations = array();
1321          if ( empty( $styles ) ) {
1322              return $declarations;
1323          }
1324  
1325          foreach ( $properties as $css_property => $value_path ) {
1326              $value = static::get_property_value( $styles, $value_path );
1327  
1328              // Look up protected properties, keyed by value path.
1329              // Skip protected properties that are explicitly set to `null`.
1330              if ( is_array( $value_path ) ) {
1331                  $path_string = implode( '.', $value_path );
1332                  if (
1333                      array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
1334                      _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
1335                  ) {
1336                      continue;
1337                  }
1338              }
1339  
1340              // Skip if empty and not "0" or value represents array of longhand values.
1341              $has_missing_value = empty( $value ) && ! is_numeric( $value );
1342              if ( $has_missing_value || is_array( $value ) ) {
1343                  continue;
1344              }
1345  
1346              $declarations[] = array(
1347                  'name'  => $css_property,
1348                  'value' => $value,
1349              );
1350          }
1351  
1352          return $declarations;
1353      }
1354  
1355      /**
1356       * Returns the style property for the given path.
1357       *
1358       * It also converts CSS Custom Property stored as
1359       * "var:preset|color|secondary" to the form
1360       * "--wp--preset--color--secondary".
1361       *
1362       * @since 5.8.0
1363       * @since 5.9.0 Added support for values of array type, which are returned as is.
1364       *
1365       * @param array $styles Styles subtree.
1366       * @param array $path   Which property to process.
1367       * @return string|array Style property value.
1368       */
1369  	protected static function get_property_value( $styles, $path ) {
1370          $value = _wp_array_get( $styles, $path, '' );
1371  
1372          if ( '' === $value || is_array( $value ) ) {
1373              return $value;
1374          }
1375  
1376          $prefix     = 'var:';
1377          $prefix_len = strlen( $prefix );
1378          $token_in   = '|';
1379          $token_out  = '--';
1380          if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
1381              $unwrapped_name = str_replace(
1382                  $token_in,
1383                  $token_out,
1384                  substr( $value, $prefix_len )
1385              );
1386              $value          = "var(--wp--$unwrapped_name)";
1387          }
1388  
1389          return $value;
1390      }
1391  
1392      /**
1393       * Builds metadata for the setting nodes, which returns in the form of:
1394       *
1395       *     [
1396       *       [
1397       *         'path'     => ['path', 'to', 'some', 'node' ],
1398       *         'selector' => 'CSS selector for some node'
1399       *       ],
1400       *       [
1401       *         'path'     => [ 'path', 'to', 'other', 'node' ],
1402       *         'selector' => 'CSS selector for other node'
1403       *       ],
1404       *     ]
1405       *
1406       * @since 5.8.0
1407       *
1408       * @param array $theme_json The tree to extract setting nodes from.
1409       * @param array $selectors  List of selectors per block.
1410       * @return array
1411       */
1412  	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
1413          $nodes = array();
1414          if ( ! isset( $theme_json['settings'] ) ) {
1415              return $nodes;
1416          }
1417  
1418          // Top-level.
1419          $nodes[] = array(
1420              'path'     => array( 'settings' ),
1421              'selector' => static::ROOT_BLOCK_SELECTOR,
1422          );
1423  
1424          // Calculate paths for blocks.
1425          if ( ! isset( $theme_json['settings']['blocks'] ) ) {
1426              return $nodes;
1427          }
1428  
1429          foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
1430              $selector = null;
1431              if ( isset( $selectors[ $name ]['selector'] ) ) {
1432                  $selector = $selectors[ $name ]['selector'];
1433              }
1434  
1435              $nodes[] = array(
1436                  'path'     => array( 'settings', 'blocks', $name ),
1437                  'selector' => $selector,
1438              );
1439          }
1440  
1441          return $nodes;
1442      }
1443  
1444      /**
1445       * Builds metadata for the style nodes, which returns in the form of:
1446       *
1447       *     [
1448       *       [
1449       *         'path'     => [ 'path', 'to', 'some', 'node' ],
1450       *         'selector' => 'CSS selector for some node',
1451       *         'duotone'  => 'CSS selector for duotone for some node'
1452       *       ],
1453       *       [
1454       *         'path'     => ['path', 'to', 'other', 'node' ],
1455       *         'selector' => 'CSS selector for other node',
1456       *         'duotone'  => null
1457       *       ],
1458       *     ]
1459       *
1460       * @since 5.8.0
1461       *
1462       * @param array $theme_json The tree to extract style nodes from.
1463       * @param array $selectors  List of selectors per block.
1464       * @return array
1465       */
1466  	protected static function get_style_nodes( $theme_json, $selectors = array() ) {
1467          $nodes = array();
1468          if ( ! isset( $theme_json['styles'] ) ) {
1469              return $nodes;
1470          }
1471  
1472          // Top-level.
1473          $nodes[] = array(
1474              'path'     => array( 'styles' ),
1475              'selector' => static::ROOT_BLOCK_SELECTOR,
1476          );
1477  
1478          if ( isset( $theme_json['styles']['elements'] ) ) {
1479              foreach ( $theme_json['styles']['elements'] as $element => $node ) {
1480                  $nodes[] = array(
1481                      'path'     => array( 'styles', 'elements', $element ),
1482                      'selector' => static::ELEMENTS[ $element ],
1483                  );
1484              }
1485          }
1486  
1487          // Blocks.
1488          if ( ! isset( $theme_json['styles']['blocks'] ) ) {
1489              return $nodes;
1490          }
1491  
1492          foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
1493              $selector = null;
1494              if ( isset( $selectors[ $name ]['selector'] ) ) {
1495                  $selector = $selectors[ $name ]['selector'];
1496              }
1497  
1498              $duotone_selector = null;
1499              if ( isset( $selectors[ $name ]['duotone'] ) ) {
1500                  $duotone_selector = $selectors[ $name ]['duotone'];
1501              }
1502  
1503              $nodes[] = array(
1504                  'path'     => array( 'styles', 'blocks', $name ),
1505                  'selector' => $selector,
1506                  'duotone'  => $duotone_selector,
1507              );
1508  
1509              if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
1510                  foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
1511                      $nodes[] = array(
1512                          'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
1513                          'selector' => $selectors[ $name ]['elements'][ $element ],
1514                      );
1515                  }
1516              }
1517          }
1518  
1519          return $nodes;
1520      }
1521  
1522      /**
1523       * For metadata values that can either be booleans or paths to booleans, gets the value.
1524       *
1525       * ```php
1526       * $data = array(
1527       *   'color' => array(
1528       *     'defaultPalette' => true
1529       *   )
1530       * );
1531       *
1532       * static::get_metadata_boolean( $data, false );
1533       * // => false
1534       *
1535       * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
1536       * // => true
1537       * ```
1538       *
1539       * @since 6.0.0
1540       *
1541       * @param array      $data    The data to inspect.
1542       * @param bool|array $path    Boolean or path to a boolean.
1543       * @param bool       $default Default value if the referenced path is missing.
1544       *                            Default false.
1545       * @return bool Value of boolean metadata.
1546       */
1547  	protected static function get_metadata_boolean( $data, $path, $default = false ) {
1548          if ( is_bool( $path ) ) {
1549              return $path;
1550          }
1551  
1552          if ( is_array( $path ) ) {
1553              $value = _wp_array_get( $data, $path );
1554              if ( null !== $value ) {
1555                  return $value;
1556              }
1557          }
1558  
1559          return $default;
1560      }
1561  
1562      /**
1563       * Merge new incoming data.
1564       *
1565       * @since 5.8.0
1566       * @since 5.9.0 Duotone preset also has origins.
1567       *
1568       * @param WP_Theme_JSON $incoming Data to merge.
1569       */
1570  	public function merge( $incoming ) {
1571          $incoming_data    = $incoming->get_raw_data();
1572          $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
1573  
1574          /*
1575           * The array_replace_recursive algorithm merges at the leaf level,
1576           * but we don't want leaf arrays to be merged, so we overwrite it.
1577           *
1578           * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
1579           * We rather replace the existing with the incoming value, if it exists.
1580           * This is the case of spacing.units.
1581           *
1582           * For leaf values that are associative arrays it will merge them as expected.
1583           * This is also not the behavior we want for the current associative arrays (presets).
1584           * We rather replace the existing with the incoming value, if it exists.
1585           * This happens, for example, when we merge data from theme.json upon existing
1586           * theme supports or when we merge anything coming from the same source twice.
1587           * This is the case of color.palette, color.gradients, color.duotone,
1588           * typography.fontSizes, or typography.fontFamilies.
1589           *
1590           * Additionally, for some preset types, we also want to make sure the
1591           * values they introduce don't conflict with default values. We do so
1592           * by checking the incoming slugs for theme presets and compare them
1593           * with the equivalent default presets: if a slug is present as a default
1594           * we remove it from the theme presets.
1595           */
1596          $nodes        = static::get_setting_nodes( $incoming_data );
1597          $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
1598          foreach ( $nodes as $node ) {
1599              $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
1600              $slugs      = array_merge_recursive( $slugs_global, $slugs_node );
1601  
1602              // Replace the spacing.units.
1603              $path    = array_merge( $node['path'], array( 'spacing', 'units' ) );
1604              $content = _wp_array_get( $incoming_data, $path, null );
1605              if ( isset( $content ) ) {
1606                  _wp_array_set( $this->theme_json, $path, $content );
1607              }
1608  
1609              // Replace the presets.
1610              foreach ( static::PRESETS_METADATA as $preset ) {
1611                  $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
1612  
1613                  foreach ( static::VALID_ORIGINS as $origin ) {
1614                      $base_path = array_merge( $node['path'], $preset['path'] );
1615                      $path      = array_merge( $base_path, array( $origin ) );
1616                      $content   = _wp_array_get( $incoming_data, $path, null );
1617                      if ( ! isset( $content ) ) {
1618                          continue;
1619                      }
1620  
1621                      if ( 'theme' === $origin && $preset['use_default_names'] ) {
1622                          foreach ( $content as &$item ) {
1623                              if ( ! array_key_exists( 'name', $item ) ) {
1624                                  $name = static::get_name_from_defaults( $item['slug'], $base_path );
1625                                  if ( null !== $name ) {
1626                                      $item['name'] = $name;
1627                                  }
1628                              }
1629                          }
1630                      }
1631  
1632                      if (
1633                          ( 'theme' !== $origin ) ||
1634                          ( 'theme' === $origin && $override_preset )
1635                      ) {
1636                          _wp_array_set( $this->theme_json, $path, $content );
1637                      } else {
1638                          $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
1639                          $content          = static::filter_slugs( $content, $slugs_for_preset );
1640                          _wp_array_set( $this->theme_json, $path, $content );
1641                      }
1642                  }
1643              }
1644          }
1645      }
1646  
1647      /**
1648       * Converts all filter (duotone) presets into SVGs.
1649       *
1650       * @since 5.9.1
1651       *
1652       * @param array $origins List of origins to process.
1653       * @return string SVG filters.
1654       */
1655  	public function get_svg_filters( $origins ) {
1656          $blocks_metadata = static::get_blocks_metadata();
1657          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
1658  
1659          $filters = '';
1660          foreach ( $setting_nodes as $metadata ) {
1661              $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1662              if ( empty( $node['color']['duotone'] ) ) {
1663                  continue;
1664              }
1665  
1666              $duotone_presets = $node['color']['duotone'];
1667  
1668              foreach ( $origins as $origin ) {
1669                  if ( ! isset( $duotone_presets[ $origin ] ) ) {
1670                      continue;
1671                  }
1672                  foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
1673                      $filters .= wp_get_duotone_filter_svg( $duotone_preset );
1674                  }
1675              }
1676          }
1677  
1678          return $filters;
1679      }
1680  
1681      /**
1682       * Returns whether a presets should be overridden or not.
1683       *
1684       * @since 5.9.0
1685       * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
1686       *
1687       * @param array      $theme_json The theme.json like structure to inspect.
1688       * @param array      $path       Path to inspect.
1689       * @param bool|array $override   Data to compute whether to override the preset.
1690       * @return boolean
1691       */
1692  	protected static function should_override_preset( $theme_json, $path, $override ) {
1693          _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
1694  
1695          if ( is_bool( $override ) ) {
1696              return $override;
1697          }
1698  
1699          /*
1700           * The relationship between whether to override the defaults
1701           * and whether the defaults are enabled is inverse:
1702           *
1703           * - If defaults are enabled  => theme presets should not be overridden
1704           * - If defaults are disabled => theme presets should be overridden
1705           *
1706           * For example, a theme sets defaultPalette to false,
1707           * making the default palette hidden from the user.
1708           * In that case, we want all the theme presets to be present,
1709           * so they should override the defaults.
1710           */
1711          if ( is_array( $override ) ) {
1712              $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
1713              if ( isset( $value ) ) {
1714                  return ! $value;
1715              }
1716  
1717              // Search the top-level key if none was found for this node.
1718              $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
1719              if ( isset( $value ) ) {
1720                  return ! $value;
1721              }
1722  
1723              return true;
1724          }
1725      }
1726  
1727      /**
1728       * Returns the default slugs for all the presets in an associative array
1729       * whose keys are the preset paths and the leafs is the list of slugs.
1730       *
1731       * For example:
1732       *
1733       *  array(
1734       *   'color' => array(
1735       *     'palette'   => array( 'slug-1', 'slug-2' ),
1736       *     'gradients' => array( 'slug-3', 'slug-4' ),
1737       *   ),
1738       * )
1739       *
1740       * @since 5.9.0
1741       *
1742       * @param array $data      A theme.json like structure.
1743       * @param array $node_path The path to inspect. It's 'settings' by default.
1744       * @return array
1745       */
1746  	protected static function get_default_slugs( $data, $node_path ) {
1747          $slugs = array();
1748  
1749          foreach ( static::PRESETS_METADATA as $metadata ) {
1750              $path   = array_merge( $node_path, $metadata['path'], array( 'default' ) );
1751              $preset = _wp_array_get( $data, $path, null );
1752              if ( ! isset( $preset ) ) {
1753                  continue;
1754              }
1755  
1756              $slugs_for_preset = array();
1757              $slugs_for_preset = array_map(
1758                  static function( $value ) {
1759                      return isset( $value['slug'] ) ? $value['slug'] : null;
1760                  },
1761                  $preset
1762              );
1763              _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
1764          }
1765  
1766          return $slugs;
1767      }
1768  
1769      /**
1770       * Get a `default`'s preset name by a provided slug.
1771       *
1772       * @since 5.9.0
1773       *
1774       * @param string $slug The slug we want to find a match from default presets.
1775       * @param array  $base_path The path to inspect. It's 'settings' by default.
1776       * @return string|null
1777       */
1778  	protected function get_name_from_defaults( $slug, $base_path ) {
1779          $path            = array_merge( $base_path, array( 'default' ) );
1780          $default_content = _wp_array_get( $this->theme_json, $path, null );
1781          if ( ! $default_content ) {
1782              return null;
1783          }
1784          foreach ( $default_content as $item ) {
1785              if ( $slug === $item['slug'] ) {
1786                  return $item['name'];
1787              }
1788          }
1789          return null;
1790      }
1791  
1792      /**
1793       * Removes the preset values whose slug is equal to any of given slugs.
1794       *
1795       * @since 5.9.0
1796       *
1797       * @param array $node  The node with the presets to validate.
1798       * @param array $slugs The slugs that should not be overridden.
1799       * @return array The new node.
1800       */
1801  	protected static function filter_slugs( $node, $slugs ) {
1802          if ( empty( $slugs ) ) {
1803              return $node;
1804          }
1805  
1806          $new_node = array();
1807          foreach ( $node as $value ) {
1808              if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
1809                  $new_node[] = $value;
1810              }
1811          }
1812  
1813          return $new_node;
1814      }
1815  
1816      /**
1817       * Removes insecure data from theme.json.
1818       *
1819       * @since 5.9.0
1820       *
1821       * @param array $theme_json Structure to sanitize.
1822       * @return array Sanitized structure.
1823       */
1824  	public static function remove_insecure_properties( $theme_json ) {
1825          $sanitized = array();
1826  
1827          $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
1828  
1829          $valid_block_names   = array_keys( static::get_blocks_metadata() );
1830          $valid_element_names = array_keys( static::ELEMENTS );
1831          $theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
1832  
1833          $blocks_metadata = static::get_blocks_metadata();
1834          $style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
1835          foreach ( $style_nodes as $metadata ) {
1836              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
1837              if ( empty( $input ) ) {
1838                  continue;
1839              }
1840  
1841              $output = static::remove_insecure_styles( $input );
1842              if ( ! empty( $output ) ) {
1843                  _wp_array_set( $sanitized, $metadata['path'], $output );
1844              }
1845          }
1846  
1847          $setting_nodes = static::get_setting_nodes( $theme_json );
1848          foreach ( $setting_nodes as $metadata ) {
1849              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
1850              if ( empty( $input ) ) {
1851                  continue;
1852              }
1853  
1854              $output = static::remove_insecure_settings( $input );
1855              if ( ! empty( $output ) ) {
1856                  _wp_array_set( $sanitized, $metadata['path'], $output );
1857              }
1858          }
1859  
1860          if ( empty( $sanitized['styles'] ) ) {
1861              unset( $theme_json['styles'] );
1862          } else {
1863              $theme_json['styles'] = $sanitized['styles'];
1864          }
1865  
1866          if ( empty( $sanitized['settings'] ) ) {
1867              unset( $theme_json['settings'] );
1868          } else {
1869              $theme_json['settings'] = $sanitized['settings'];
1870          }
1871  
1872          return $theme_json;
1873      }
1874  
1875      /**
1876       * Processes a setting node and returns the same node
1877       * without the insecure settings.
1878       *
1879       * @since 5.9.0
1880       *
1881       * @param array $input Node to process.
1882       * @return array
1883       */
1884  	protected static function remove_insecure_settings( $input ) {
1885          $output = array();
1886          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1887              foreach ( static::VALID_ORIGINS as $origin ) {
1888                  $path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) );
1889                  $presets          = _wp_array_get( $input, $path_with_origin, null );
1890                  if ( null === $presets ) {
1891                      continue;
1892                  }
1893  
1894                  $escaped_preset = array();
1895                  foreach ( $presets as $preset ) {
1896                      if (
1897                          esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
1898                          sanitize_html_class( $preset['slug'] ) === $preset['slug']
1899                      ) {
1900                          $value = null;
1901                          if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
1902                              $value = $preset[ $preset_metadata['value_key'] ];
1903                          } elseif (
1904                              isset( $preset_metadata['value_func'] ) &&
1905                              is_callable( $preset_metadata['value_func'] )
1906                          ) {
1907                              $value = call_user_func( $preset_metadata['value_func'], $preset );
1908                          }
1909  
1910                          $preset_is_valid = true;
1911                          foreach ( $preset_metadata['properties'] as $property ) {
1912                              if ( ! static::is_safe_css_declaration( $property, $value ) ) {
1913                                  $preset_is_valid = false;
1914                                  break;
1915                              }
1916                          }
1917  
1918                          if ( $preset_is_valid ) {
1919                              $escaped_preset[] = $preset;
1920                          }
1921                      }
1922                  }
1923  
1924                  if ( ! empty( $escaped_preset ) ) {
1925                      _wp_array_set( $output, $path_with_origin, $escaped_preset );
1926                  }
1927              }
1928          }
1929          return $output;
1930      }
1931  
1932      /**
1933       * Processes a style node and returns the same node
1934       * without the insecure styles.
1935       *
1936       * @since 5.9.0
1937       *
1938       * @param array $input Node to process.
1939       * @return array
1940       */
1941  	protected static function remove_insecure_styles( $input ) {
1942          $output       = array();
1943          $declarations = static::compute_style_properties( $input );
1944  
1945          foreach ( $declarations as $declaration ) {
1946              if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
1947                  $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
1948  
1949                  // Check the value isn't an array before adding so as to not
1950                  // double up shorthand and longhand styles.
1951                  $value = _wp_array_get( $input, $path, array() );
1952                  if ( ! is_array( $value ) ) {
1953                      _wp_array_set( $output, $path, $value );
1954                  }
1955              }
1956          }
1957          return $output;
1958      }
1959  
1960      /**
1961       * Checks that a declaration provided by the user is safe.
1962       *
1963       * @since 5.9.0
1964       *
1965       * @param string $property_name  Property name in a CSS declaration, i.e. the `color` in `color: red`.
1966       * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
1967       * @return bool
1968       */
1969  	protected static function is_safe_css_declaration( $property_name, $property_value ) {
1970          $style_to_validate = $property_name . ': ' . $property_value;
1971          $filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
1972          return ! empty( trim( $filtered ) );
1973      }
1974  
1975      /**
1976       * Returns the raw data.
1977       *
1978       * @since 5.8.0
1979       *
1980       * @return array Raw data.
1981       */
1982  	public function get_raw_data() {
1983          return $this->theme_json;
1984      }
1985  
1986      /**
1987       * Transforms the given editor settings according the
1988       * add_theme_support format to the theme.json format.
1989       *
1990       * @since 5.8.0
1991       *
1992       * @param array $settings Existing editor settings.
1993       * @return array Config that adheres to the theme.json schema.
1994       */
1995  	public static function get_from_editor_settings( $settings ) {
1996          $theme_settings = array(
1997              'version'  => static::LATEST_SCHEMA,
1998              'settings' => array(),
1999          );
2000  
2001          // Deprecated theme supports.
2002          if ( isset( $settings['disableCustomColors'] ) ) {
2003              if ( ! isset( $theme_settings['settings']['color'] ) ) {
2004                  $theme_settings['settings']['color'] = array();
2005              }
2006              $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
2007          }
2008  
2009          if ( isset( $settings['disableCustomGradients'] ) ) {
2010              if ( ! isset( $theme_settings['settings']['color'] ) ) {
2011                  $theme_settings['settings']['color'] = array();
2012              }
2013              $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
2014          }
2015  
2016          if ( isset( $settings['disableCustomFontSizes'] ) ) {
2017              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
2018                  $theme_settings['settings']['typography'] = array();
2019              }
2020              $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
2021          }
2022  
2023          if ( isset( $settings['enableCustomLineHeight'] ) ) {
2024              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
2025                  $theme_settings['settings']['typography'] = array();
2026              }
2027              $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
2028          }
2029  
2030          if ( isset( $settings['enableCustomUnits'] ) ) {
2031              if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
2032                  $theme_settings['settings']['spacing'] = array();
2033              }
2034              $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
2035                  array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
2036                  $settings['enableCustomUnits'];
2037          }
2038  
2039          if ( isset( $settings['colors'] ) ) {
2040              if ( ! isset( $theme_settings['settings']['color'] ) ) {
2041                  $theme_settings['settings']['color'] = array();
2042              }
2043              $theme_settings['settings']['color']['palette'] = $settings['colors'];
2044          }
2045  
2046          if ( isset( $settings['gradients'] ) ) {
2047              if ( ! isset( $theme_settings['settings']['color'] ) ) {
2048                  $theme_settings['settings']['color'] = array();
2049              }
2050              $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
2051          }
2052  
2053          if ( isset( $settings['fontSizes'] ) ) {
2054              $font_sizes = $settings['fontSizes'];
2055              // Back-compatibility for presets without units.
2056              foreach ( $font_sizes as $key => $font_size ) {
2057                  if ( is_numeric( $font_size['size'] ) ) {
2058                      $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
2059                  }
2060              }
2061              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
2062                  $theme_settings['settings']['typography'] = array();
2063              }
2064              $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
2065          }
2066  
2067          if ( isset( $settings['enableCustomSpacing'] ) ) {
2068              if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
2069                  $theme_settings['settings']['spacing'] = array();
2070              }
2071              $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
2072          }
2073  
2074          return $theme_settings;
2075      }
2076  
2077      /**
2078       * Returns the current theme's wanted patterns(slugs) to be
2079       * registered from Pattern Directory.
2080       *
2081       * @since 6.0.0
2082       *
2083       * @return string[]
2084       */
2085  	public function get_patterns() {
2086          if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
2087              return $this->theme_json['patterns'];
2088          }
2089          return array();
2090      }
2091  
2092      /**
2093       * Returns a valid theme.json as provided by a theme.
2094       *
2095       * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
2096       * This also uses appearanceTools instead of their opt-ins if all of them are true.
2097       *
2098       * @since 6.0.0
2099       *
2100       * @return array
2101       */
2102  	public function get_data() {
2103          $output = $this->theme_json;
2104          $nodes  = static::get_setting_nodes( $output );
2105  
2106          /**
2107           * Flatten the theme & custom origins into a single one.
2108           *
2109           * For example, the following:
2110           *
2111           * {
2112           *   "settings": {
2113           *     "color": {
2114           *       "palette": {
2115           *         "theme": [ {} ],
2116           *         "custom": [ {} ]
2117           *       }
2118           *     }
2119           *   }
2120           * }
2121           *
2122           * will be converted to:
2123           *
2124           * {
2125           *   "settings": {
2126           *     "color": {
2127           *       "palette": [ {} ]
2128           *     }
2129           *   }
2130           * }
2131           */
2132          foreach ( $nodes as $node ) {
2133              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2134                  $path   = array_merge( $node['path'], $preset_metadata['path'] );
2135                  $preset = _wp_array_get( $output, $path, null );
2136                  if ( null === $preset ) {
2137                      continue;
2138                  }
2139  
2140                  $items = array();
2141                  if ( isset( $preset['theme'] ) ) {
2142                      foreach ( $preset['theme'] as $item ) {
2143                          $slug = $item['slug'];
2144                          unset( $item['slug'] );
2145                          $items[ $slug ] = $item;
2146                      }
2147                  }
2148                  if ( isset( $preset['custom'] ) ) {
2149                      foreach ( $preset['custom'] as $item ) {
2150                          $slug = $item['slug'];
2151                          unset( $item['slug'] );
2152                          $items[ $slug ] = $item;
2153                      }
2154                  }
2155                  $flattened_preset = array();
2156                  foreach ( $items as $slug => $value ) {
2157                      $flattened_preset[] = array_merge( array( 'slug' => $slug ), $value );
2158                  }
2159                  _wp_array_set( $output, $path, $flattened_preset );
2160              }
2161          }
2162  
2163          // If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
2164          // this code unsets them and sets 'appearanceTools' instead.
2165          foreach ( $nodes as $node ) {
2166              $all_opt_ins_are_set = true;
2167              foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
2168                  $full_path = array_merge( $node['path'], $opt_in_path );
2169                  // Use "unset prop" as a marker instead of "null" because
2170                  // "null" can be a valid value for some props (e.g. blockGap).
2171                  $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
2172                  if ( 'unset prop' === $opt_in_value ) {
2173                      $all_opt_ins_are_set = false;
2174                      break;
2175                  }
2176              }
2177  
2178              if ( $all_opt_ins_are_set ) {
2179                  _wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true );
2180                  foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
2181                      $full_path = array_merge( $node['path'], $opt_in_path );
2182                      // Use "unset prop" as a marker instead of "null" because
2183                      // "null" can be a valid value for some props (e.g. blockGap).
2184                      $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
2185                      if ( true !== $opt_in_value ) {
2186                          continue;
2187                      }
2188  
2189                      // The following could be improved to be path independent.
2190                      // At the moment it relies on a couple of assumptions:
2191                      //
2192                      // - all opt-ins having a path of size 2.
2193                      // - there's two sources of settings: the top-level and the block-level.
2194                      if (
2195                          ( 1 === count( $node['path'] ) ) &&
2196                          ( 'settings' === $node['path'][0] )
2197                      ) {
2198                          // Top-level settings.
2199                          unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
2200                          if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
2201                              unset( $output['settings'][ $opt_in_path[0] ] );
2202                          }
2203                      } elseif (
2204                          ( 3 === count( $node['path'] ) ) &&
2205                          ( 'settings' === $node['path'][0] ) &&
2206                          ( 'blocks' === $node['path'][1] )
2207                      ) {
2208                          // Block-level settings.
2209                          $block_name = $node['path'][2];
2210                          unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
2211                          if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
2212                              unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
2213                          }
2214                      }
2215                  }
2216              }
2217          }
2218  
2219          wp_recursive_ksort( $output );
2220  
2221          return $output;
2222      }
2223  
2224  }


Generated: Thu Apr 25 01:00:03 2024 Cross-referenced by PHPXref 0.7.1