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


Generated: Sat Oct 23 01:00:08 2021 Cross-referenced by PHPXref 0.7.1