[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Dec 22 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |