| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WP_Theme_JSON_Resolver class 4 * 5 * @package WordPress 6 * @subpackage Theme 7 * @since 5.8.0 8 */ 9 10 /** 11 * Class that abstracts the processing of the different data sources 12 * for site-level config and offers an API to work with them. 13 * 14 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes). 15 * This is a low-level API that may need to do breaking changes. Please, 16 * use get_global_settings(), get_global_styles(), and get_global_stylesheet() instead. 17 * 18 * @access private 19 */ 20 #[AllowDynamicProperties] 21 class WP_Theme_JSON_Resolver { 22 23 /** 24 * Container for keep track of registered blocks. 25 * 26 * @since 6.1.0 27 * @since 7.2.0 Added the `merged` entry. 28 * @var array 29 */ 30 protected static $blocks_cache = array( 31 'core' => array(), 32 'blocks' => array(), 33 'theme' => array(), 34 'user' => array(), 35 'merged' => array(), 36 ); 37 38 /** 39 * Merged data keyed by origin. 40 * 41 * @since 7.2.0 42 * @var array 43 */ 44 protected static $merged = array(); 45 46 /** 47 * Container for data coming from core. 48 * 49 * @since 5.8.0 50 * @var WP_Theme_JSON 51 */ 52 protected static $core = null; 53 54 /** 55 * Container for data coming from the blocks. 56 * 57 * @since 6.1.0 58 * @var WP_Theme_JSON 59 */ 60 protected static $blocks = null; 61 62 /** 63 * Container for data coming from the theme. 64 * 65 * @since 5.8.0 66 * @var WP_Theme_JSON 67 */ 68 protected static $theme = null; 69 70 /** 71 * Container for data coming from the user. 72 * 73 * @since 5.9.0 74 * @var WP_Theme_JSON 75 */ 76 protected static $user = null; 77 78 /** 79 * Stores the ID of the custom post type 80 * that holds the user data. 81 * 82 * @since 5.9.0 83 * @var int 84 */ 85 protected static $user_custom_post_type_id = null; 86 87 /** 88 * Container to keep loaded i18n schema for `theme.json`. 89 * 90 * @since 5.8.0 As `$theme_json_i18n`. 91 * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`. 92 * @var array 93 */ 94 protected static $i18n_schema = null; 95 96 /** 97 * `theme.json` file cache. 98 * 99 * @since 6.1.0 100 * @var array 101 */ 102 protected static $theme_json_file_cache = array(); 103 104 /** 105 * Processes a file that adheres to the theme.json schema 106 * and returns an array with its contents, or a void array if none found. 107 * 108 * @since 5.8.0 109 * @since 6.1.0 Added caching. 110 * 111 * @param string $file_path Path to file. Empty if no file. 112 * @return array Contents that adhere to the theme.json schema. 113 */ 114 protected static function read_json_file( $file_path ) { 115 if ( $file_path ) { 116 if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) { 117 return static::$theme_json_file_cache[ $file_path ]; 118 } 119 120 $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); 121 if ( is_array( $decoded_file ) ) { 122 static::$theme_json_file_cache[ $file_path ] = $decoded_file; 123 return static::$theme_json_file_cache[ $file_path ]; 124 } 125 } 126 127 return array(); 128 } 129 130 /** 131 * Returns a data structure used in theme.json translation. 132 * 133 * @since 5.8.0 134 * @deprecated 5.9.0 135 * 136 * @return array An array of theme.json fields that are translatable and the keys that are translatable. 137 */ 138 public static function get_fields_to_translate() { 139 _deprecated_function( __METHOD__, '5.9.0' ); 140 return array(); 141 } 142 143 /** 144 * Given a theme.json structure modifies it in place to update certain values 145 * by its translated strings according to the language set by the user. 146 * 147 * @since 5.8.0 148 * 149 * @param array $theme_json The theme.json to translate. 150 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. 151 * Default 'default'. 152 * @return array Returns the modified $theme_json_structure. 153 */ 154 protected static function translate( $theme_json, $domain = 'default' ) { 155 if ( null === static::$i18n_schema ) { 156 $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); 157 static::$i18n_schema = $i18n_schema ?? array(); 158 } 159 160 return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); 161 } 162 163 /** 164 * Returns core's origin config. 165 * 166 * @since 5.8.0 167 * 168 * @return WP_Theme_JSON Entity that holds core data. 169 */ 170 public static function get_core_data() { 171 if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) { 172 return static::$core; 173 } 174 175 $config = static::read_json_file( __DIR__ . '/theme.json' ); 176 $config = static::translate( $config ); 177 178 /** 179 * Filters the default data provided by WordPress for global styles & settings. 180 * 181 * @since 6.1.0 182 * 183 * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data. 184 */ 185 $theme_json = apply_filters( 'wp_theme_json_data_default', new WP_Theme_JSON_Data( $config, 'default' ) ); 186 187 /* 188 * Backward compatibility for extenders returning a WP_Theme_JSON_Data 189 * compatible class that is not a WP_Theme_JSON_Data object. 190 */ 191 if ( $theme_json instanceof WP_Theme_JSON_Data ) { 192 static::$core = $theme_json->get_theme_json(); 193 } else { 194 $config = $theme_json->get_data(); 195 static::$core = new WP_Theme_JSON( $config, 'default' ); 196 } 197 198 return static::$core; 199 } 200 201 /** 202 * Checks whether the registered blocks were already processed for this origin. 203 * 204 * @since 6.1.0 205 * @since 7.2.0 Added the 'merged' origin. 206 * 207 * @param string $origin Data source for which to cache the blocks. 208 * Valid values are 'core', 'blocks', 'theme', 'user', 209 * and 'merged'. 210 * @return bool True on success, false otherwise. 211 */ 212 protected static function has_same_registered_blocks( $origin ) { 213 // Bail out if the origin is invalid. 214 if ( ! isset( static::$blocks_cache[ $origin ] ) ) { 215 return false; 216 } 217 218 $registry = WP_Block_Type_Registry::get_instance(); 219 $blocks = $registry->get_all_registered(); 220 221 // Is there metadata for all currently registered blocks? 222 $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] ); 223 if ( empty( $block_diff ) ) { 224 return true; 225 } 226 227 foreach ( $blocks as $block_name => $block_type ) { 228 static::$blocks_cache[ $origin ][ $block_name ] = true; 229 } 230 231 return false; 232 } 233 234 /** 235 * Returns the theme's data. 236 * 237 * Data from theme.json will be backfilled from existing 238 * theme supports, if any. Note that if the same data 239 * is present in theme.json and in theme supports, 240 * the theme.json takes precedence. 241 * 242 * @since 5.8.0 243 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed. 244 * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports. 245 * @since 6.6.0 Add support for 'default-font-sizes' and 'default-spacing-sizes' theme supports. 246 * Added registration and merging of block style variations from partial theme.json files and the block styles registry. 247 * 248 * @param array $deprecated Deprecated. Not used. 249 * @param array $options { 250 * Options arguments. 251 * 252 * @type bool $with_supports Whether to include theme supports in the data. Default true. 253 * } 254 * @return WP_Theme_JSON Entity that holds theme data. 255 * 256 * @phpstan-param array{} $deprecated 257 */ 258 public static function get_theme_data( $deprecated = array(), $options = array() ) { 259 if ( ! empty( $deprecated ) ) { 260 _deprecated_argument( __METHOD__, '5.9.0' ); 261 } 262 263 $options = wp_parse_args( $options, array( 'with_supports' => true ) ); 264 265 if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { 266 $wp_theme = wp_get_theme(); 267 $theme_json_file = $wp_theme->get_file_path( 'theme.json' ); 268 if ( is_readable( $theme_json_file ) ) { 269 $theme_json_data = static::read_json_file( $theme_json_file ); 270 $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); 271 } else { 272 $theme_json_data = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); 273 } 274 275 /* 276 * Register variations defined by theme partials (theme.json files in the styles directory). 277 * This is required so the variations pass sanitization of theme.json data. 278 */ 279 $variations = static::get_style_variations( 'block' ); 280 wp_register_block_style_variations_from_theme_json_partials( $variations ); 281 282 /* 283 * Source variations from the block registry and block style variation files. Then, merge them into the existing theme.json data. 284 * 285 * In case the same style properties are defined in several sources, this is how we should resolve the values, 286 * from higher to lower priority: 287 * 288 * - styles.blocks.blockType.variations from theme.json 289 * - styles.variations from theme.json 290 * - variations from block style variation files 291 * - variations from block styles registry 292 * 293 * See test_add_registered_block_styles_to_theme_data and test_unwraps_block_style_variations. 294 * 295 */ 296 $theme_json_data = self::inject_variations_from_block_style_variation_files( $theme_json_data, $variations ); 297 $theme_json_data = self::inject_variations_from_block_styles_registry( $theme_json_data ); 298 299 /** 300 * Filters the data provided by the theme for global styles and settings. 301 * 302 * @since 6.1.0 303 * 304 * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data. 305 */ 306 $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) ); 307 308 /* 309 * Backward compatibility for extenders returning a WP_Theme_JSON_Data 310 * compatible class that is not a WP_Theme_JSON_Data object. 311 */ 312 if ( $theme_json instanceof WP_Theme_JSON_Data ) { 313 static::$theme = $theme_json->get_theme_json(); 314 } else { 315 $config = $theme_json->get_data(); 316 static::$theme = new WP_Theme_JSON( $config ); 317 } 318 319 if ( $wp_theme->parent() ) { 320 // Get parent theme.json. 321 $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' ); 322 if ( $theme_json_file !== $parent_theme_json_file && is_readable( $parent_theme_json_file ) ) { 323 $parent_theme_json_data = static::read_json_file( $parent_theme_json_file ); 324 $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) ); 325 $parent_theme = new WP_Theme_JSON( $parent_theme_json_data ); 326 327 /* 328 * Merge the child theme.json into the parent theme.json. 329 * The child theme takes precedence over the parent. 330 */ 331 $parent_theme->merge( static::$theme ); 332 static::$theme = $parent_theme; 333 } 334 } 335 } 336 337 if ( ! $options['with_supports'] ) { 338 return static::$theme; 339 } 340 341 /* 342 * We want the presets and settings declared in theme.json 343 * to override the ones declared via theme supports. 344 * So we take theme supports, transform it to theme.json shape 345 * and merge the static::$theme upon that. 346 */ 347 $theme_support_data = WP_Theme_JSON::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() ); 348 if ( ! wp_theme_has_theme_json() ) { 349 /* 350 * Unlike block themes, classic themes without a theme.json disable 351 * default presets when custom preset theme support is added. This 352 * behavior can be overridden by using the corresponding default 353 * preset theme support. 354 */ 355 $theme_support_data['settings']['color']['defaultPalette'] = 356 ! isset( $theme_support_data['settings']['color']['palette'] ) || 357 current_theme_supports( 'default-color-palette' ); 358 $theme_support_data['settings']['color']['defaultGradients'] = 359 ! isset( $theme_support_data['settings']['color']['gradients'] ) || 360 current_theme_supports( 'default-gradient-presets' ); 361 $theme_support_data['settings']['typography']['defaultFontSizes'] = 362 ! isset( $theme_support_data['settings']['typography']['fontSizes'] ) || 363 current_theme_supports( 'default-font-sizes' ); 364 $theme_support_data['settings']['spacing']['defaultSpacingSizes'] = 365 ! isset( $theme_support_data['settings']['spacing']['spacingSizes'] ) || 366 current_theme_supports( 'default-spacing-sizes' ); 367 368 /* 369 * Shadow presets are explicitly disabled for classic themes until a 370 * decision is made for whether the default presets should match the 371 * other presets or if they should be disabled by default in classic 372 * themes. See https://github.com/WordPress/gutenberg/issues/59989. 373 */ 374 $theme_support_data['settings']['shadow']['defaultPresets'] = false; 375 376 // Allow themes to enable link color setting via theme_support. 377 if ( current_theme_supports( 'link-color' ) ) { 378 $theme_support_data['settings']['color']['link'] = true; 379 } 380 381 // Allow themes to enable all border settings via theme_support. 382 if ( current_theme_supports( 'border' ) ) { 383 $theme_support_data['settings']['border']['color'] = true; 384 $theme_support_data['settings']['border']['radius'] = true; 385 $theme_support_data['settings']['border']['style'] = true; 386 $theme_support_data['settings']['border']['width'] = true; 387 } 388 389 // Allow themes to enable appearance tools via theme_support. 390 if ( current_theme_supports( 'appearance-tools' ) ) { 391 $theme_support_data['settings']['appearanceTools'] = true; 392 } 393 } 394 $with_theme_supports = new WP_Theme_JSON( $theme_support_data ); 395 $with_theme_supports->merge( static::$theme ); 396 return $with_theme_supports; 397 } 398 399 /** 400 * Gets the styles for blocks from the block.json file. 401 * 402 * @since 6.1.0 403 * 404 * @return WP_Theme_JSON 405 */ 406 public static function get_block_data() { 407 $registry = WP_Block_Type_Registry::get_instance(); 408 $blocks = $registry->get_all_registered(); 409 410 if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) { 411 return static::$blocks; 412 } 413 414 $config = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); 415 foreach ( $blocks as $block_name => $block_type ) { 416 if ( isset( $block_type->supports['__experimentalStyle'] ) ) { 417 $config['styles']['blocks'][ $block_name ] = self::remove_json_comments( $block_type->supports['__experimentalStyle'] ); 418 } 419 420 if ( 421 isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) && 422 ! isset( $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] ) 423 ) { 424 /* 425 * Ensure an empty placeholder value exists for the block, if it provides a default blockGap value. 426 * The real blockGap value to be used will be determined when the styles are rendered for output. 427 */ 428 $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null; 429 } 430 } 431 432 /** 433 * Filters the data provided by the blocks for global styles & settings. 434 * 435 * @since 6.1.0 436 * 437 * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data. 438 */ 439 $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data( $config, 'blocks' ) ); 440 441 /* 442 * Backward compatibility for extenders returning a WP_Theme_JSON_Data 443 * compatible class that is not a WP_Theme_JSON_Data object. 444 */ 445 if ( $theme_json instanceof WP_Theme_JSON_Data ) { 446 static::$blocks = $theme_json->get_theme_json(); 447 } else { 448 $config = $theme_json->get_data(); 449 static::$blocks = new WP_Theme_JSON( $config, 'blocks' ); 450 } 451 452 return static::$blocks; 453 } 454 455 /** 456 * When given an array, this will remove any keys with the name `//`. 457 * 458 * @since 6.1.0 459 * 460 * @param array $input_array The array to filter. 461 * @return array The filtered array. 462 */ 463 private static function remove_json_comments( $input_array ) { 464 unset( $input_array['//'] ); 465 foreach ( $input_array as $k => $v ) { 466 if ( is_array( $v ) ) { 467 $input_array[ $k ] = self::remove_json_comments( $v ); 468 } 469 } 470 471 return $input_array; 472 } 473 474 /** 475 * Returns the custom post type that contains the user's origin config 476 * for the active theme or an empty array if none are found. 477 * 478 * This can also create and return a new draft custom post type. 479 * 480 * @since 5.9.0 481 * 482 * @param WP_Theme $theme The theme object. If empty, it 483 * defaults to the active theme. 484 * @param bool $create_post Optional. Whether a new custom post 485 * type should be created if none are 486 * found. Default false. 487 * @param array $post_status_filter Optional. Filter custom post type by 488 * post status. Default `array( 'publish' )`, 489 * so it only fetches published posts. 490 * @return array Custom Post Type for the user's origin config. 491 */ 492 public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { 493 if ( ! $theme instanceof WP_Theme ) { 494 $theme = wp_get_theme(); 495 } 496 497 $user_cpt = array(); 498 $post_type_filter = 'wp_global_styles'; 499 $stylesheet = $theme->get_stylesheet(); 500 $args = array( 501 'posts_per_page' => 1, 502 'orderby' => 'date', 503 'order' => 'desc', 504 'post_type' => $post_type_filter, 505 'post_status' => $post_status_filter, 506 'ignore_sticky_posts' => true, 507 'no_found_rows' => true, 508 'update_post_meta_cache' => false, 509 'update_post_term_cache' => false, 510 'tax_query' => array( 511 array( 512 'taxonomy' => 'wp_theme', 513 'field' => 'name', 514 'terms' => $stylesheet, 515 ), 516 ), 517 ); 518 519 $global_style_query = new WP_Query(); 520 $recent_posts = $global_style_query->query( $args ); 521 if ( count( $recent_posts ) === 1 && $recent_posts[0] instanceof WP_Post ) { 522 $user_cpt = get_object_vars( $recent_posts[0] ); 523 } elseif ( $create_post ) { 524 $cpt_post_id = wp_insert_post( 525 array( 526 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', 527 'post_status' => 'publish', 528 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518. 529 'post_type' => $post_type_filter, 530 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ), 531 'tax_input' => array( 532 'wp_theme' => array( $stylesheet ), 533 ), 534 ), 535 true 536 ); 537 if ( ! is_wp_error( $cpt_post_id ) ) { 538 $post = get_post( $cpt_post_id ); 539 if ( $post instanceof WP_Post ) { 540 $user_cpt = get_object_vars( $post ); 541 } 542 } 543 } 544 545 return $user_cpt; 546 } 547 548 /** 549 * Returns the user's origin config. 550 * 551 * @since 5.9.0 552 * @since 6.6.0 The 'isGlobalStylesUserThemeJSON' flag is left on the user data. 553 * Register the block style variations coming from the user data. 554 * 555 * @return WP_Theme_JSON Entity that holds styles for user data. 556 */ 557 public static function get_user_data() { 558 if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) { 559 return static::$user; 560 } 561 562 $config = array(); 563 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); 564 565 if ( array_key_exists( 'post_content', $user_cpt ) ) { 566 $decoded_data = json_decode( $user_cpt['post_content'], true ); 567 568 $json_decoding_error = json_last_error(); 569 if ( JSON_ERROR_NONE !== $json_decoding_error ) { 570 wp_trigger_error( __METHOD__, 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); 571 /** 572 * Filters the data provided by the user for global styles & settings. 573 * 574 * @since 6.1.0 575 * 576 * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data. 577 */ 578 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data( $config, 'custom' ) ); 579 580 /* 581 * Backward compatibility for extenders returning a WP_Theme_JSON_Data 582 * compatible class that is not a WP_Theme_JSON_Data object. 583 */ 584 if ( $theme_json instanceof WP_Theme_JSON_Data ) { 585 return $theme_json->get_theme_json(); 586 } else { 587 $config = $theme_json->get_data(); 588 return new WP_Theme_JSON( $config, 'custom' ); 589 } 590 } 591 592 /* 593 * Very important to verify that the flag isGlobalStylesUserThemeJSON is true. 594 * If it's not true then the content was not escaped and is not safe. 595 */ 596 if ( 597 is_array( $decoded_data ) && 598 isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && 599 $decoded_data['isGlobalStylesUserThemeJSON'] 600 ) { 601 unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); 602 $config = $decoded_data; 603 } 604 } 605 606 /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */ 607 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data( $config, 'custom' ) ); 608 609 /* 610 * Backward compatibility for extenders returning a WP_Theme_JSON_Data 611 * compatible class that is not a WP_Theme_JSON_Data object. 612 */ 613 if ( $theme_json instanceof WP_Theme_JSON_Data ) { 614 static::$user = $theme_json->get_theme_json(); 615 } else { 616 $config = $theme_json->get_data(); 617 static::$user = new WP_Theme_JSON( $config, 'custom' ); 618 } 619 620 return static::$user; 621 } 622 623 /** 624 * Returns the data merged from multiple origins. 625 * 626 * There are four sources of data (origins) for a site: 627 * 628 * - default => WordPress 629 * - blocks => each one of the blocks provides data for itself 630 * - theme => the active theme 631 * - custom => data provided by the user 632 * 633 * The custom's has higher priority than the theme's, the theme's higher than blocks', 634 * and block's higher than default's. 635 * 636 * Unlike the getters 637 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data}, 638 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data}, 639 * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data}, 640 * this method returns data after it has been merged with the previous origins. 641 * This means that if the same piece of data is declared in different origins 642 * (default, blocks, theme, custom), the last origin overrides the previous. 643 * 644 * For example, if the user has set a background color 645 * for the paragraph block, and the theme has done it as well, 646 * the user preference wins. 647 * 648 * @since 5.8.0 649 * @since 5.9.0 Added user data, removed the `$settings` parameter, 650 * added the `$origin` parameter. 651 * @since 6.1.0 Added block data and generation of spacingSizes array. 652 * @since 6.2.0 Changed ' $origin' parameter values to 'default', 'blocks', 'theme' or 'custom'. 653 * @since 7.2.0 Caches the merged data per origin for the request and returns a copy of it. 654 * 655 * @param string $origin Optional. To what level should we merge data: 'default', 'blocks', 'theme' or 'custom'. 656 * 'custom' is used as default value as well as fallback value if the origin is unknown. 657 * @return WP_Theme_JSON 658 */ 659 public static function get_merged_data( $origin = 'custom' ) { 660 if ( is_array( $origin ) ) { 661 _deprecated_argument( __FUNCTION__, '5.9.0' ); 662 } 663 664 if ( ! in_array( $origin, array( 'default', 'blocks', 'theme', 'custom' ), true ) ) { 665 $origin = 'custom'; 666 } 667 668 /* 669 * Discard the merged data when blocks were registered after the last merge. 670 * 671 * This check detects new blocks, not theme support changes. Call 672 * clean_cached_data() after changing theme supports to refresh the merged data. 673 */ 674 if ( ! static::has_same_registered_blocks( 'merged' ) ) { 675 static::$merged = array(); 676 } 677 if ( isset( static::$merged[ $origin ] ) ) { 678 return clone static::$merged[ $origin ]; 679 } 680 681 $result = new WP_Theme_JSON(); 682 $result->merge( static::get_core_data() ); 683 if ( 'default' !== $origin ) { 684 $result->merge( static::get_block_data() ); 685 } 686 if ( 'theme' === $origin || 'custom' === $origin ) { 687 $result->merge( static::get_theme_data() ); 688 } 689 if ( 'custom' === $origin ) { 690 $result->merge( static::get_user_data() ); 691 } 692 693 static::$merged[ $origin ] = $result; 694 695 // Return a clone so callers can modify the result without changing the cache. 696 return clone $result; 697 } 698 699 /** 700 * Returns the ID of the custom post type 701 * that stores user data. 702 * 703 * @since 5.9.0 704 * 705 * @return int|null ID for a post of type `wp_global_styles`, or null if not available. 706 */ 707 public static function get_user_global_styles_post_id() { 708 if ( null !== static::$user_custom_post_type_id ) { 709 return static::$user_custom_post_type_id; 710 } 711 712 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true ); 713 714 if ( array_key_exists( 'ID', $user_cpt ) ) { 715 static::$user_custom_post_type_id = $user_cpt['ID']; 716 } 717 718 return static::$user_custom_post_type_id; 719 } 720 721 /** 722 * Determines whether the active theme has a theme.json file. 723 * 724 * @since 5.8.0 725 * @since 5.9.0 Added a check in the parent theme. 726 * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead. 727 * 728 * @return bool Whether the active theme has a theme.json file. 729 */ 730 public static function theme_has_support() { 731 _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' ); 732 733 return wp_theme_has_theme_json(); 734 } 735 736 /** 737 * Builds the path to the given file and checks that it is readable. 738 * 739 * If it isn't, returns an empty string, otherwise returns the whole file path. 740 * 741 * @since 5.8.0 742 * @since 5.9.0 Adapted to work with child themes, added the `$template` argument. 743 * 744 * @param string $file_name Name of the file. 745 * @param bool $template Optional. Use template theme directory. Default false. 746 * @return string The whole file path or empty if the file doesn't exist. 747 */ 748 protected static function get_file_path_from_theme( $file_name, $template = false ) { 749 $path = $template ? get_template_directory() : get_stylesheet_directory(); 750 $candidate = $path . '/' . $file_name; 751 752 return is_readable( $candidate ) ? $candidate : ''; 753 } 754 755 /** 756 * Cleans the cached data so it can be recalculated. 757 * 758 * @since 5.8.0 759 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`, 760 * and `$i18n_schema` variables to reset. 761 * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables 762 * to reset. 763 * @since 7.2.0 Added the `$merged` variable to reset. 764 */ 765 public static function clean_cached_data() { 766 static::$core = null; 767 static::$blocks = null; 768 static::$blocks_cache = array_fill_keys( array_keys( static::$blocks_cache ), array() ); 769 static::$merged = array(); 770 static::$theme = null; 771 static::$user = null; 772 static::$user_custom_post_type_id = null; 773 static::$i18n_schema = null; 774 } 775 776 /** 777 * Returns an array of all nested JSON files within a given directory. 778 * 779 * @since 6.2.0 780 * 781 * @param string $dir The directory to recursively iterate and list files of. 782 * @return array The merged array. 783 */ 784 private static function recursively_iterate_json( $dir ) { 785 $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) ); 786 $nested_json_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) ); 787 return $nested_json_files; 788 } 789 790 /** 791 * Determines if a supplied style variation matches the provided scope. 792 * 793 * For backwards compatibility, if a variation does not define any scope 794 * related property, e.g. `blockTypes`, it is assumed to be a theme style 795 * variation. 796 * 797 * @since 6.6.0 798 * 799 * @param array $variation Theme.json shaped style variation object. 800 * @param string $scope Scope to check e.g. theme, block etc. 801 * @return bool Whether the supplied style variation matches the provided scope. 802 */ 803 private static function style_variation_has_scope( $variation, $scope ) { 804 if ( 'block' === $scope ) { 805 return isset( $variation['blockTypes'] ); 806 } 807 808 if ( 'theme' === $scope ) { 809 return ! isset( $variation['blockTypes'] ); 810 } 811 812 return false; 813 } 814 815 /** 816 * Returns the style variations defined by the theme. 817 * 818 * @since 6.0.0 819 * @since 6.2.0 Returns parent theme variations if theme is a child. 820 * @since 6.6.0 Added configurable scope parameter to allow filtering 821 * theme.json partial files by the scope to which they 822 * can be applied e.g. theme vs block etc. 823 * Added basic caching for read theme.json partial files. 824 * 825 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc. 826 * @return array The style variations defined by the theme. 827 */ 828 public static function get_style_variations( $scope = 'theme' ) { 829 $variation_files = array(); 830 $variations = array(); 831 $base_directory = get_stylesheet_directory() . '/styles'; 832 $template_directory = get_template_directory() . '/styles'; 833 if ( is_dir( $base_directory ) ) { 834 $variation_files = self::recursively_iterate_json( $base_directory ); 835 } 836 if ( is_dir( $template_directory ) && $template_directory !== $base_directory ) { 837 $variation_files_parent = self::recursively_iterate_json( $template_directory ); 838 // If the child and parent variation file basename are the same, only include the child theme's. 839 foreach ( $variation_files_parent as $parent_path => $parent ) { 840 foreach ( $variation_files as $child_path => $child ) { 841 if ( basename( $parent_path ) === basename( $child_path ) ) { 842 unset( $variation_files_parent[ $parent_path ] ); 843 } 844 } 845 } 846 $variation_files = array_merge( $variation_files, $variation_files_parent ); 847 } 848 ksort( $variation_files ); 849 foreach ( $variation_files as $path => $file ) { 850 $decoded_file = self::read_json_file( $path ); 851 if ( is_array( $decoded_file ) && self::style_variation_has_scope( $decoded_file, $scope ) ) { 852 $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); 853 $variation = ( new WP_Theme_JSON( $translated ) )->get_raw_data(); 854 if ( empty( $variation['title'] ) ) { 855 $variation['title'] = basename( $path, '.json' ); 856 } 857 $variations[] = $variation; 858 } 859 } 860 return $variations; 861 } 862 863 /** 864 * Resolves relative paths in theme.json styles to theme absolute paths 865 * and returns them in an array that can be embedded 866 * as the value of `_link` object in REST API responses. 867 * 868 * @since 6.6.0 869 * @since 6.7.0 Resolve relative paths in block styles. 870 * 871 * @param WP_Theme_JSON $theme_json A theme json instance. 872 * @return array An array of resolved paths. 873 */ 874 public static function get_resolved_theme_uris( $theme_json ) { 875 $resolved_theme_uris = array(); 876 877 if ( ! $theme_json instanceof WP_Theme_JSON ) { 878 return $resolved_theme_uris; 879 } 880 881 $theme_json_data = $theme_json->get_raw_data(); 882 /* 883 * The same file convention when registering web fonts. 884 * See: WP_Font_Face_Resolver::to_theme_file_uri. 885 */ 886 $placeholder = 'file:./'; 887 888 // Top level styles. 889 $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null; 890 if ( 891 isset( $background_image_url ) && 892 is_string( $background_image_url ) && 893 // Skip if the src doesn't start with the placeholder, as there's nothing to replace. 894 str_starts_with( $background_image_url, $placeholder ) 895 ) { 896 $file_type = wp_check_filetype( $background_image_url ); 897 $src_url = str_replace( $placeholder, '', $background_image_url ); 898 $resolved_theme_uri = array( 899 'name' => $background_image_url, 900 'href' => sanitize_url( get_theme_file_uri( $src_url ) ), 901 'target' => 'styles.background.backgroundImage.url', 902 ); 903 if ( isset( $file_type['type'] ) ) { 904 $resolved_theme_uri['type'] = $file_type['type']; 905 } 906 $resolved_theme_uris[] = $resolved_theme_uri; 907 } 908 909 // Block styles. 910 if ( ! empty( $theme_json_data['styles']['blocks'] ) ) { 911 foreach ( $theme_json_data['styles']['blocks'] as $block_name => $block_styles ) { 912 if ( ! isset( $block_styles['background']['backgroundImage']['url'] ) ) { 913 continue; 914 } 915 $background_image_url = $block_styles['background']['backgroundImage']['url']; 916 if ( 917 is_string( $background_image_url ) && 918 // Skip if the src doesn't start with the placeholder, as there's nothing to replace. 919 str_starts_with( $background_image_url, $placeholder ) 920 ) { 921 $file_type = wp_check_filetype( $background_image_url ); 922 $src_url = str_replace( $placeholder, '', $background_image_url ); 923 $resolved_theme_uri = array( 924 'name' => $background_image_url, 925 'href' => sanitize_url( get_theme_file_uri( $src_url ) ), 926 'target' => "styles.blocks.{$block_name}.background.backgroundImage.url", 927 ); 928 if ( isset( $file_type['type'] ) ) { 929 $resolved_theme_uri['type'] = $file_type['type']; 930 } 931 $resolved_theme_uris[] = $resolved_theme_uri; 932 } 933 } 934 } 935 936 return $resolved_theme_uris; 937 } 938 939 /** 940 * Resolves relative paths in theme.json styles to theme absolute paths 941 * and merges them with incoming theme JSON. 942 * 943 * @since 6.6.0 944 * 945 * @param WP_Theme_JSON $theme_json A theme json instance. 946 * @return WP_Theme_JSON Theme merged with resolved paths, if any found. 947 */ 948 public static function resolve_theme_file_uris( $theme_json ) { 949 $resolved_urls = static::get_resolved_theme_uris( $theme_json ); 950 if ( empty( $resolved_urls ) ) { 951 return $theme_json; 952 } 953 954 $resolved_theme_json_data = $theme_json->get_raw_data(); 955 956 foreach ( $resolved_urls as $resolved_url ) { 957 $path = explode( '.', $resolved_url['target'] ); 958 _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] ); 959 } 960 961 return new WP_Theme_JSON( $resolved_theme_json_data ); 962 } 963 964 /** 965 * Adds variations sourced from block style variations files to the supplied theme.json data. 966 * 967 * @since 6.6.0 968 * 969 * @param array $data Array following the theme.json specification. 970 * @param array $variations Shared block style variations. 971 * @return array Theme json data including shared block style variation definitions. 972 */ 973 private static function inject_variations_from_block_style_variation_files( $data, $variations ) { 974 if ( empty( $variations ) ) { 975 return $data; 976 } 977 978 foreach ( $variations as $variation ) { 979 if ( empty( $variation['styles'] ) || empty( $variation['blockTypes'] ) ) { 980 continue; 981 } 982 983 $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] ); 984 985 foreach ( $variation['blockTypes'] as $block_type ) { 986 // First, override partial styles with any top-level styles. 987 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array(); 988 if ( ! empty( $top_level_data ) ) { 989 $variation['styles'] = array_replace_recursive( $variation['styles'], $top_level_data ); 990 } 991 992 // Then, override styles so far with any block-level styles. 993 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); 994 if ( ! empty( $block_level_data ) ) { 995 $variation['styles'] = array_replace_recursive( $variation['styles'], $block_level_data ); 996 } 997 998 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name ); 999 _wp_array_set( $data, $path, $variation['styles'] ); 1000 } 1001 } 1002 1003 return $data; 1004 } 1005 1006 /** 1007 * Adds variations sourced from the block styles registry to the supplied theme.json data. 1008 * 1009 * @since 6.6.0 1010 * 1011 * @param array $data Array following the theme.json specification. 1012 * @return array Theme json data including shared block style variation definitions. 1013 */ 1014 private static function inject_variations_from_block_styles_registry( $data ) { 1015 $registry = WP_Block_Styles_Registry::get_instance(); 1016 $styles = $registry->get_all_registered(); 1017 1018 foreach ( $styles as $block_type => $variations ) { 1019 foreach ( $variations as $variation_name => $variation ) { 1020 if ( empty( $variation['style_data'] ) ) { 1021 continue; 1022 } 1023 1024 // First, override registry styles with any top-level styles. 1025 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array(); 1026 if ( ! empty( $top_level_data ) ) { 1027 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $top_level_data ); 1028 } 1029 1030 // Then, override styles so far with any block-level styles. 1031 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); 1032 if ( ! empty( $block_level_data ) ) { 1033 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $block_level_data ); 1034 } 1035 1036 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name ); 1037 _wp_array_set( $data, $path, $variation['style_data'] ); 1038 } 1039 } 1040 1041 return $data; 1042 } 1043 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Oct 30 08:20:34 2026 | Cross-referenced by PHPXref |