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