[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[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 #[AllowDynamicProperties] 20 class WP_Theme_JSON { 21 22 /** 23 * Container of data in theme.json format. 24 * 25 * @since 5.8.0 26 * @var array 27 */ 28 protected $theme_json = null; 29 30 /** 31 * Holds block metadata extracted from block.json 32 * to be shared among all instances so we don't 33 * process it twice. 34 * 35 * @since 5.8.0 36 * @since 6.1.0 Initialize as an empty array. 37 * @var array 38 */ 39 protected static $blocks_metadata = array(); 40 41 /** 42 * The CSS selector for the top-level styles. 43 * 44 * @since 5.8.0 45 * @var string 46 */ 47 const ROOT_BLOCK_SELECTOR = 'body'; 48 49 /** 50 * The sources of data this object can represent. 51 * 52 * @since 5.8.0 53 * @since 6.1.0 Added 'blocks'. 54 * @var string[] 55 */ 56 const VALID_ORIGINS = array( 57 'default', 58 'blocks', 59 'theme', 60 'custom', 61 ); 62 63 /** 64 * Presets are a set of values that serve 65 * to bootstrap some styles: colors, font sizes, etc. 66 * 67 * They are a unkeyed array of values such as: 68 * 69 * array( 70 * array( 71 * 'slug' => 'unique-name-within-the-set', 72 * 'name' => 'Name for the UI', 73 * <value_key> => 'value' 74 * ), 75 * ) 76 * 77 * This contains the necessary metadata to process them: 78 * 79 * - path => Where to find the preset within the settings section. 80 * - prevent_override => Disables override of default presets by theme presets. 81 * The relationship between whether to override the defaults 82 * and whether the defaults are enabled is inverse: 83 * - If defaults are enabled => theme presets should not be overriden 84 * - If defaults are disabled => theme presets should be overriden 85 * For example, a theme sets defaultPalette to false, 86 * making the default palette hidden from the user. 87 * In that case, we want all the theme presets to be present, 88 * so they should override the defaults by setting this false. 89 * - use_default_names => whether to use the default names 90 * - value_key => the key that represents the value 91 * - value_func => optionally, instead of value_key, a function to generate 92 * the value that takes a preset as an argument 93 * (either value_key or value_func should be present) 94 * - css_vars => template string to use in generating the CSS Custom Property. 95 * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined 96 * substituting the $slug for the slug's value for each preset value. 97 * - classes => array containing a structure with the classes to 98 * generate for the presets, where for each array item 99 * the key is the class name and the value the property name. 100 * The "$slug" substring will be replaced by the slug of each preset. 101 * For example: 102 * 'classes' => array( 103 * '.has-$slug-color' => 'color', 104 * '.has-$slug-background-color' => 'background-color', 105 * '.has-$slug-border-color' => 'border-color', 106 * ) 107 * - properties => array of CSS properties to be used by kses to 108 * validate the content of each preset 109 * by means of the remove_insecure_properties method. 110 * 111 * @since 5.8.0 112 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets, 113 * `use_default_names` preset key, and simplified the metadata structure. 114 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the 115 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`. 116 * @since 6.2.0 Added 'shadow' presets. 117 * @var array 118 */ 119 const PRESETS_METADATA = array( 120 array( 121 'path' => array( 'color', 'palette' ), 122 'prevent_override' => array( 'color', 'defaultPalette' ), 123 'use_default_names' => false, 124 'value_key' => 'color', 125 'css_vars' => '--wp--preset--color--$slug', 126 'classes' => array( 127 '.has-$slug-color' => 'color', 128 '.has-$slug-background-color' => 'background-color', 129 '.has-$slug-border-color' => 'border-color', 130 ), 131 'properties' => array( 'color', 'background-color', 'border-color' ), 132 ), 133 array( 134 'path' => array( 'color', 'gradients' ), 135 'prevent_override' => array( 'color', 'defaultGradients' ), 136 'use_default_names' => false, 137 'value_key' => 'gradient', 138 'css_vars' => '--wp--preset--gradient--$slug', 139 'classes' => array( '.has-$slug-gradient-background' => 'background' ), 140 'properties' => array( 'background' ), 141 ), 142 array( 143 'path' => array( 'color', 'duotone' ), 144 'prevent_override' => array( 'color', 'defaultDuotone' ), 145 'use_default_names' => false, 146 'value_func' => 'wp_get_duotone_filter_property', 147 'css_vars' => '--wp--preset--duotone--$slug', 148 'classes' => array(), 149 'properties' => array( 'filter' ), 150 ), 151 array( 152 'path' => array( 'typography', 'fontSizes' ), 153 'prevent_override' => false, 154 'use_default_names' => true, 155 'value_func' => 'wp_get_typography_font_size_value', 156 'css_vars' => '--wp--preset--font-size--$slug', 157 'classes' => array( '.has-$slug-font-size' => 'font-size' ), 158 'properties' => array( 'font-size' ), 159 ), 160 array( 161 'path' => array( 'typography', 'fontFamilies' ), 162 'prevent_override' => false, 163 'use_default_names' => false, 164 'value_key' => 'fontFamily', 165 'css_vars' => '--wp--preset--font-family--$slug', 166 'classes' => array( '.has-$slug-font-family' => 'font-family' ), 167 'properties' => array( 'font-family' ), 168 ), 169 array( 170 'path' => array( 'spacing', 'spacingSizes' ), 171 'prevent_override' => false, 172 'use_default_names' => true, 173 'value_key' => 'size', 174 'css_vars' => '--wp--preset--spacing--$slug', 175 'classes' => array(), 176 'properties' => array( 'padding', 'margin' ), 177 ), 178 array( 179 'path' => array( 'shadow', 'presets' ), 180 'prevent_override' => array( 'shadow', 'defaultPresets' ), 181 'use_default_names' => false, 182 'value_key' => 'shadow', 183 'css_vars' => '--wp--preset--shadow--$slug', 184 'classes' => array(), 185 'properties' => array( 'box-shadow' ), 186 ), 187 ); 188 189 /** 190 * Metadata for style properties. 191 * 192 * Each element is a direct mapping from the CSS property name to the 193 * path to the value in theme.json & block attributes. 194 * 195 * @since 5.8.0 196 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`, 197 * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`, 198 * `text-decoration`, `text-transform`, and `filter` properties, 199 * simplified the metadata structure. 200 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`, 201 * `--wp--style--root--padding-*`, and `box-shadow` properties, 202 * removed the `--wp--style--block-gap` property. 203 * @since 6.2.0 Added `outline-*`, and `min-height` properties. 204 * 205 * @var array 206 */ 207 const PROPERTIES_METADATA = array( 208 'background' => array( 'color', 'gradient' ), 209 'background-color' => array( 'color', 'background' ), 210 'border-radius' => array( 'border', 'radius' ), 211 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 212 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), 213 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), 214 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), 215 'border-color' => array( 'border', 'color' ), 216 'border-width' => array( 'border', 'width' ), 217 'border-style' => array( 'border', 'style' ), 218 'border-top-color' => array( 'border', 'top', 'color' ), 219 'border-top-width' => array( 'border', 'top', 'width' ), 220 'border-top-style' => array( 'border', 'top', 'style' ), 221 'border-right-color' => array( 'border', 'right', 'color' ), 222 'border-right-width' => array( 'border', 'right', 'width' ), 223 'border-right-style' => array( 'border', 'right', 'style' ), 224 'border-bottom-color' => array( 'border', 'bottom', 'color' ), 225 'border-bottom-width' => array( 'border', 'bottom', 'width' ), 226 'border-bottom-style' => array( 'border', 'bottom', 'style' ), 227 'border-left-color' => array( 'border', 'left', 'color' ), 228 'border-left-width' => array( 'border', 'left', 'width' ), 229 'border-left-style' => array( 'border', 'left', 'style' ), 230 'color' => array( 'color', 'text' ), 231 'font-family' => array( 'typography', 'fontFamily' ), 232 'font-size' => array( 'typography', 'fontSize' ), 233 'font-style' => array( 'typography', 'fontStyle' ), 234 'font-weight' => array( 'typography', 'fontWeight' ), 235 'letter-spacing' => array( 'typography', 'letterSpacing' ), 236 'line-height' => array( 'typography', 'lineHeight' ), 237 'margin' => array( 'spacing', 'margin' ), 238 'margin-top' => array( 'spacing', 'margin', 'top' ), 239 'margin-right' => array( 'spacing', 'margin', 'right' ), 240 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), 241 'margin-left' => array( 'spacing', 'margin', 'left' ), 242 'min-height' => array( 'dimensions', 'minHeight' ), 243 'outline-color' => array( 'outline', 'color' ), 244 'outline-offset' => array( 'outline', 'offset' ), 245 'outline-style' => array( 'outline', 'style' ), 246 'outline-width' => array( 'outline', 'width' ), 247 'padding' => array( 'spacing', 'padding' ), 248 'padding-top' => array( 'spacing', 'padding', 'top' ), 249 'padding-right' => array( 'spacing', 'padding', 'right' ), 250 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), 251 'padding-left' => array( 'spacing', 'padding', 'left' ), 252 '--wp--style--root--padding' => array( 'spacing', 'padding' ), 253 '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ), 254 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ), 255 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ), 256 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ), 257 'text-decoration' => array( 'typography', 'textDecoration' ), 258 'text-transform' => array( 'typography', 'textTransform' ), 259 'filter' => array( 'filter', 'duotone' ), 260 'box-shadow' => array( 'shadow' ), 261 ); 262 263 /** 264 * Indirect metadata for style properties that are not directly output. 265 * 266 * Each element maps from a CSS property name to an array of 267 * paths to the value in theme.json & block attributes. 268 * 269 * Indirect properties are not output directly by `compute_style_properties`, 270 * but are used elsewhere in the processing of global styles. The indirect 271 * property is used to validate whether or not a style value is allowed. 272 * 273 * @since 6.2.0 274 * 275 * @var array 276 */ 277 const INDIRECT_PROPERTIES_METADATA = array( 278 'gap' => array( 279 array( 'spacing', 'blockGap' ), 280 ), 281 'column-gap' => array( 282 array( 'spacing', 'blockGap', 'left' ), 283 ), 284 'row-gap' => array( 285 array( 'spacing', 'blockGap', 'top' ), 286 ), 287 'max-width' => array( 288 array( 'layout', 'contentSize' ), 289 array( 'layout', 'wideSize' ), 290 ), 291 ); 292 293 /** 294 * Protected style properties. 295 * 296 * These style properties are only rendered if a setting enables it 297 * via a value other than `null`. 298 * 299 * Each element maps the style property to the corresponding theme.json 300 * setting key. 301 * 302 * @since 5.9.0 303 */ 304 const PROTECTED_PROPERTIES = array( 305 'spacing.blockGap' => array( 'spacing', 'blockGap' ), 306 ); 307 308 /** 309 * The top-level keys a theme.json can have. 310 * 311 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`. 312 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, 313 * added the `customTemplates` and `templateParts` values. 314 * @var string[] 315 */ 316 const VALID_TOP_LEVEL_KEYS = array( 317 'customTemplates', 318 'patterns', 319 'settings', 320 'styles', 321 'templateParts', 322 'title', 323 'version', 324 ); 325 326 /** 327 * The valid properties under the settings key. 328 * 329 * @since 5.8.0 As `ALLOWED_SETTINGS`. 330 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, 331 * added new properties for `border`, `color`, `spacing`, 332 * and `typography`, and renamed others according to the new schema. 333 * @since 6.0.0 Added `color.defaultDuotone`. 334 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. 335 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets', 336 * `position.fixed` and `position.sticky`. 337 * @var array 338 */ 339 const VALID_SETTINGS = array( 340 'appearanceTools' => null, 341 'useRootPaddingAwareAlignments' => null, 342 'border' => array( 343 'color' => null, 344 'radius' => null, 345 'style' => null, 346 'width' => null, 347 ), 348 'color' => array( 349 'background' => null, 350 'custom' => null, 351 'customDuotone' => null, 352 'customGradient' => null, 353 'defaultDuotone' => null, 354 'defaultGradients' => null, 355 'defaultPalette' => null, 356 'duotone' => null, 357 'gradients' => null, 358 'link' => null, 359 'palette' => null, 360 'text' => null, 361 ), 362 'custom' => null, 363 'dimensions' => array( 364 'minHeight' => null, 365 ), 366 'layout' => array( 367 'contentSize' => null, 368 'definitions' => null, 369 'wideSize' => null, 370 ), 371 'position' => array( 372 'fixed' => null, 373 'sticky' => null, 374 ), 375 'spacing' => array( 376 'customSpacingSize' => null, 377 'spacingSizes' => null, 378 'spacingScale' => null, 379 'blockGap' => null, 380 'margin' => null, 381 'padding' => null, 382 'units' => null, 383 ), 384 'shadow' => array( 385 'presets' => null, 386 'defaultPresets' => null, 387 ), 388 'typography' => array( 389 'fluid' => null, 390 'customFontSize' => null, 391 'dropCap' => null, 392 'fontFamilies' => null, 393 'fontSizes' => null, 394 'fontStyle' => null, 395 'fontWeight' => null, 396 'letterSpacing' => null, 397 'lineHeight' => null, 398 'textDecoration' => null, 399 'textTransform' => null, 400 ), 401 ); 402 403 /** 404 * The valid properties under the styles key. 405 * 406 * @since 5.8.0 As `ALLOWED_STYLES`. 407 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, 408 * added new properties for `border`, `filter`, `spacing`, 409 * and `typography`. 410 * @since 6.1.0 Added new side properties for `border`, 411 * added new property `shadow`, 412 * updated `blockGap` to be allowed at any level. 413 * @since 6.2.0 Added `outline`, and `minHeight` properties. 414 * 415 * @var array 416 */ 417 const VALID_STYLES = array( 418 'border' => array( 419 'color' => null, 420 'radius' => null, 421 'style' => null, 422 'width' => null, 423 'top' => null, 424 'right' => null, 425 'bottom' => null, 426 'left' => null, 427 ), 428 'color' => array( 429 'background' => null, 430 'gradient' => null, 431 'text' => null, 432 ), 433 'dimensions' => array( 434 'minHeight' => null, 435 ), 436 'filter' => array( 437 'duotone' => null, 438 ), 439 'outline' => array( 440 'color' => null, 441 'offset' => null, 442 'style' => null, 443 'width' => null, 444 ), 445 'shadow' => null, 446 'spacing' => array( 447 'margin' => null, 448 'padding' => null, 449 'blockGap' => null, 450 ), 451 'typography' => array( 452 'fontFamily' => null, 453 'fontSize' => null, 454 'fontStyle' => null, 455 'fontWeight' => null, 456 'letterSpacing' => null, 457 'lineHeight' => null, 458 'textDecoration' => null, 459 'textTransform' => null, 460 ), 461 'css' => null, 462 ); 463 464 /** 465 * Defines which pseudo selectors are enabled for which elements. 466 * 467 * The order of the selectors should be: link, any-link, visited, hover, focus, active. 468 * This is to ensure the user action (hover, focus and active) styles have a higher 469 * specificity than the visited styles, which in turn have a higher specificity than 470 * the unvisited styles. 471 * 472 * See https://core.trac.wordpress.org/ticket/56928. 473 * Note: this will affect both top-level and block-level elements. 474 * 475 * @since 6.1.0 476 * @since 6.2.0 Added support for ':link' and ':any-link'. 477 */ 478 const VALID_ELEMENT_PSEUDO_SELECTORS = array( 479 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), 480 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), 481 ); 482 483 /** 484 * The valid elements that can be found under styles. 485 * 486 * @since 5.8.0 487 * @since 6.1.0 Added `heading`, `button`, and `caption` elements. 488 * @var string[] 489 */ 490 const ELEMENTS = array( 491 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity. 492 'heading' => 'h1, h2, h3, h4, h5, h6', 493 'h1' => 'h1', 494 'h2' => 'h2', 495 'h3' => 'h3', 496 'h4' => 'h4', 497 'h5' => 'h5', 498 'h6' => 'h6', 499 // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. 500 'button' => '.wp-element-button, .wp-block-button__link', 501 // The block classes are necessary to target older content that won't use the new class names. 502 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', 503 'cite' => 'cite', 504 ); 505 506 const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( 507 'button' => 'wp-element-button', 508 'caption' => 'wp-element-caption', 509 ); 510 511 /** 512 * List of block support features that can have their related styles 513 * generated under their own feature level selector rather than the block's. 514 * 515 * @since 6.1.0 516 * @var string[] 517 */ 518 const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( 519 '__experimentalBorder' => 'border', 520 'color' => 'color', 521 'spacing' => 'spacing', 522 'typography' => 'typography', 523 ); 524 525 /** 526 * Returns a class name by an element name. 527 * 528 * @since 6.1.0 529 * 530 * @param string $element The name of the element. 531 * @return string The name of the class. 532 */ 533 public static function get_element_class_name( $element ) { 534 $class_name = ''; 535 536 // TODO: Replace array_key_exists() with isset() check once WordPress drops 537 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 538 if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) { 539 $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ]; 540 } 541 542 return $class_name; 543 } 544 545 /** 546 * Options that settings.appearanceTools enables. 547 * 548 * @since 6.0.0 549 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`. 550 * @var array 551 */ 552 const APPEARANCE_TOOLS_OPT_INS = array( 553 array( 'border', 'color' ), 554 array( 'border', 'radius' ), 555 array( 'border', 'style' ), 556 array( 'border', 'width' ), 557 array( 'color', 'link' ), 558 array( 'dimensions', 'minHeight' ), 559 array( 'position', 'sticky' ), 560 array( 'spacing', 'blockGap' ), 561 array( 'spacing', 'margin' ), 562 array( 'spacing', 'padding' ), 563 array( 'typography', 'lineHeight' ), 564 ); 565 566 /** 567 * The latest version of the schema in use. 568 * 569 * @since 5.8.0 570 * @since 5.9.0 Changed value from 1 to 2. 571 * @var int 572 */ 573 const LATEST_SCHEMA = 2; 574 575 /** 576 * Constructor. 577 * 578 * @since 5.8.0 579 * 580 * @param array $theme_json A structure that follows the theme.json schema. 581 * @param string $origin Optional. What source of data this object represents. 582 * One of 'default', 'theme', or 'custom'. Default 'theme'. 583 */ 584 public function __construct( $theme_json = array(), $origin = 'theme' ) { 585 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { 586 $origin = 'theme'; 587 } 588 589 $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); 590 $valid_block_names = array_keys( static::get_blocks_metadata() ); 591 $valid_element_names = array_keys( static::ELEMENTS ); 592 $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); 593 $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); 594 595 // Internally, presets are keyed by origin. 596 $nodes = static::get_setting_nodes( $this->theme_json ); 597 foreach ( $nodes as $node ) { 598 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 599 $path = $node['path']; 600 foreach ( $preset_metadata['path'] as $subpath ) { 601 $path[] = $subpath; 602 } 603 $preset = _wp_array_get( $this->theme_json, $path, null ); 604 if ( null !== $preset ) { 605 // If the preset is not already keyed by origin. 606 if ( isset( $preset[0] ) || empty( $preset ) ) { 607 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); 608 } 609 } 610 } 611 } 612 } 613 614 /** 615 * Enables some opt-in settings if theme declared support. 616 * 617 * @since 5.9.0 618 * 619 * @param array $theme_json A theme.json structure to modify. 620 * @return array The modified theme.json structure. 621 */ 622 protected static function maybe_opt_in_into_settings( $theme_json ) { 623 $new_theme_json = $theme_json; 624 625 if ( 626 isset( $new_theme_json['settings']['appearanceTools'] ) && 627 true === $new_theme_json['settings']['appearanceTools'] 628 ) { 629 static::do_opt_in_into_settings( $new_theme_json['settings'] ); 630 } 631 632 if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { 633 foreach ( $new_theme_json['settings']['blocks'] as &$block ) { 634 if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { 635 static::do_opt_in_into_settings( $block ); 636 } 637 } 638 } 639 640 return $new_theme_json; 641 } 642 643 /** 644 * Enables some settings. 645 * 646 * @since 5.9.0 647 * 648 * @param array $context The context to which the settings belong. 649 */ 650 protected static function do_opt_in_into_settings( &$context ) { 651 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) { 652 // Use "unset prop" as a marker instead of "null" because 653 // "null" can be a valid value for some props (e.g. blockGap). 654 if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { 655 _wp_array_set( $context, $path, true ); 656 } 657 } 658 659 unset( $context['appearanceTools'] ); 660 } 661 662 /** 663 * Sanitizes the input according to the schemas. 664 * 665 * @since 5.8.0 666 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. 667 * 668 * @param array $input Structure to sanitize. 669 * @param array $valid_block_names List of valid block names. 670 * @param array $valid_element_names List of valid element names. 671 * @return array The sanitized output. 672 */ 673 protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { 674 675 $output = array(); 676 677 if ( ! is_array( $input ) ) { 678 return $output; 679 } 680 681 // Preserve only the top most level keys. 682 $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); 683 684 /* 685 * Remove any rules that are annotated as "top" in VALID_STYLES constant. 686 * Some styles are only meant to be available at the top-level (e.g.: blockGap), 687 * hence, the schema for blocks & elements should not have them. 688 */ 689 $styles_non_top_level = static::VALID_STYLES; 690 foreach ( array_keys( $styles_non_top_level ) as $section ) { 691 // array_key_exists() needs to be used instead of isset() because the value can be null. 692 if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { 693 foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { 694 if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { 695 unset( $styles_non_top_level[ $section ][ $prop ] ); 696 } 697 } 698 } 699 } 700 701 // Build the schema based on valid block & element names. 702 $schema = array(); 703 $schema_styles_elements = array(); 704 705 /* 706 * Set allowed element pseudo selectors based on per element allow list. 707 * Target data structure in schema: 708 * e.g. 709 * - top level elements: `$schema['styles']['elements']['link'][':hover']`. 710 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. 711 */ 712 foreach ( $valid_element_names as $element ) { 713 $schema_styles_elements[ $element ] = $styles_non_top_level; 714 715 // TODO: Replace array_key_exists() with isset() check once WordPress drops 716 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 717 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { 718 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 719 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; 720 } 721 } 722 } 723 724 $schema_styles_blocks = array(); 725 $schema_settings_blocks = array(); 726 foreach ( $valid_block_names as $block ) { 727 // Build the schema for each block style variation. 728 $style_variation_names = array(); 729 if ( 730 ! empty( $input['styles']['blocks'][ $block ]['variations'] ) && 731 is_array( $input['styles']['blocks'][ $block ]['variations'] ) 732 ) { 733 $style_variation_names = array_keys( $input['styles']['blocks'][ $block ]['variations'] ); 734 } 735 736 $schema_styles_variations = array(); 737 if ( ! empty( $style_variation_names ) ) { 738 $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level ); 739 } 740 741 $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; 742 $schema_styles_blocks[ $block ] = $styles_non_top_level; 743 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; 744 $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; 745 } 746 747 $schema['styles'] = static::VALID_STYLES; 748 $schema['styles']['blocks'] = $schema_styles_blocks; 749 $schema['styles']['elements'] = $schema_styles_elements; 750 $schema['settings'] = static::VALID_SETTINGS; 751 $schema['settings']['blocks'] = $schema_settings_blocks; 752 753 // Remove anything that's not present in the schema. 754 foreach ( array( 'styles', 'settings' ) as $subtree ) { 755 if ( ! isset( $input[ $subtree ] ) ) { 756 continue; 757 } 758 759 if ( ! is_array( $input[ $subtree ] ) ) { 760 unset( $output[ $subtree ] ); 761 continue; 762 } 763 764 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); 765 766 if ( empty( $result ) ) { 767 unset( $output[ $subtree ] ); 768 } else { 769 $output[ $subtree ] = $result; 770 } 771 } 772 773 return $output; 774 } 775 776 /** 777 * Appends a sub-selector to an existing one. 778 * 779 * Given the compounded $selector "h1, h2, h3" 780 * and the $to_append selector ".some-class" the result will be 781 * "h1.some-class, h2.some-class, h3.some-class". 782 * 783 * @since 5.8.0 784 * @since 6.1.0 Added append position. 785 * 786 * @param string $selector Original selector. 787 * @param string $to_append Selector to append. 788 * @param string $position A position sub-selector should be appended. Default 'right'. 789 * @return string The new selector. 790 */ 791 protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { 792 $new_selectors = array(); 793 $selectors = explode( ',', $selector ); 794 foreach ( $selectors as $sel ) { 795 $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; 796 } 797 return implode( ',', $new_selectors ); 798 } 799 800 /** 801 * Returns the metadata for each block. 802 * 803 * Example: 804 * 805 * { 806 * 'core/paragraph': { 807 * 'selector': 'p', 808 * 'elements': { 809 * 'link' => 'link selector', 810 * 'etc' => 'element selector' 811 * } 812 * }, 813 * 'core/heading': { 814 * 'selector': 'h1', 815 * 'elements': {} 816 * }, 817 * 'core/image': { 818 * 'selector': '.wp-block-image', 819 * 'duotone': 'img', 820 * 'elements': {} 821 * } 822 * } 823 * 824 * @since 5.8.0 825 * @since 5.9.0 Added `duotone` key with CSS selector. 826 * @since 6.1.0 Added `features` key with block support feature level selectors. 827 * 828 * @return array Block metadata. 829 */ 830 protected static function get_blocks_metadata() { 831 $registry = WP_Block_Type_Registry::get_instance(); 832 $blocks = $registry->get_all_registered(); 833 834 // Is there metadata for all currently registered blocks? 835 $blocks = array_diff_key( $blocks, static::$blocks_metadata ); 836 if ( empty( $blocks ) ) { 837 return static::$blocks_metadata; 838 } 839 840 foreach ( $blocks as $block_name => $block_type ) { 841 if ( 842 isset( $block_type->supports['__experimentalSelector'] ) && 843 is_string( $block_type->supports['__experimentalSelector'] ) 844 ) { 845 static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; 846 } else { 847 static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); 848 } 849 850 if ( 851 isset( $block_type->supports['color']['__experimentalDuotone'] ) && 852 is_string( $block_type->supports['color']['__experimentalDuotone'] ) 853 ) { 854 static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; 855 } 856 857 // Generate block support feature level selectors if opted into 858 // for the current block. 859 $features = array(); 860 foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { 861 if ( 862 isset( $block_type->supports[ $key ]['__experimentalSelector'] ) && 863 $block_type->supports[ $key ]['__experimentalSelector'] 864 ) { 865 $features[ $feature ] = static::scope_selector( 866 static::$blocks_metadata[ $block_name ]['selector'], 867 $block_type->supports[ $key ]['__experimentalSelector'] 868 ); 869 } 870 } 871 872 if ( ! empty( $features ) ) { 873 static::$blocks_metadata[ $block_name ]['features'] = $features; 874 } 875 876 // Assign defaults, then overwrite those that the block sets by itself. 877 // If the block selector is compounded, will append the element to each 878 // individual block selector. 879 $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); 880 foreach ( static::ELEMENTS as $el_name => $el_selector ) { 881 $element_selector = array(); 882 foreach ( $block_selectors as $selector ) { 883 if ( $selector === $el_selector ) { 884 $element_selector = array( $el_selector ); 885 break; 886 } 887 $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); 888 } 889 static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); 890 } 891 // If the block has style variations, append their selectors to the block metadata. 892 if ( ! empty( $block_type->styles ) ) { 893 $style_selectors = array(); 894 foreach ( $block_type->styles as $style ) { 895 // The style variation classname is duplicated in the selector to ensure that it overrides core block styles. 896 $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); 897 } 898 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; 899 } 900 } 901 902 return static::$blocks_metadata; 903 } 904 905 /** 906 * Given a tree, removes the keys that are not present in the schema. 907 * 908 * It is recursive and modifies the input in-place. 909 * 910 * @since 5.8.0 911 * 912 * @param array $tree Input to process. 913 * @param array $schema Schema to adhere to. 914 * @return array The modified $tree. 915 */ 916 protected static function remove_keys_not_in_schema( $tree, $schema ) { 917 $tree = array_intersect_key( $tree, $schema ); 918 919 foreach ( $schema as $key => $data ) { 920 if ( ! isset( $tree[ $key ] ) ) { 921 continue; 922 } 923 924 if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { 925 $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); 926 927 if ( empty( $tree[ $key ] ) ) { 928 unset( $tree[ $key ] ); 929 } 930 } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { 931 unset( $tree[ $key ] ); 932 } 933 } 934 935 return $tree; 936 } 937 938 /** 939 * Returns the existing settings for each block. 940 * 941 * Example: 942 * 943 * { 944 * 'root': { 945 * 'color': { 946 * 'custom': true 947 * } 948 * }, 949 * 'core/paragraph': { 950 * 'spacing': { 951 * 'customPadding': true 952 * } 953 * } 954 * } 955 * 956 * @since 5.8.0 957 * 958 * @return array Settings per block. 959 */ 960 public function get_settings() { 961 if ( ! isset( $this->theme_json['settings'] ) ) { 962 return array(); 963 } else { 964 return $this->theme_json['settings']; 965 } 966 } 967 968 /** 969 * Returns the stylesheet that results of processing 970 * the theme.json structure this object represents. 971 * 972 * @since 5.8.0 973 * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters. 974 * 975 * @param string[] $types Types of styles to load. Will load all by default. It accepts: 976 * - `variables`: only the CSS Custom Properties for presets & custom ones. 977 * - `styles`: only the styles section in theme.json. 978 * - `presets`: only the classes for the presets. 979 * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS. 980 * @param array $options An array of options for now used for internal purposes only (may change without notice). 981 * The options currently supported are 'scope' that makes sure all style are scoped to a 982 * given selector, and root_selector which overwrites and forces a given selector to be 983 * used on the root node. 984 * @return string The resulting stylesheet. 985 */ 986 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { 987 if ( null === $origins ) { 988 $origins = static::VALID_ORIGINS; 989 } 990 991 if ( is_string( $types ) ) { 992 // Dispatch error and map old arguments to new ones. 993 _deprecated_argument( __FUNCTION__, '5.9.0' ); 994 if ( 'block_styles' === $types ) { 995 $types = array( 'styles', 'presets' ); 996 } elseif ( 'css_variables' === $types ) { 997 $types = array( 'variables' ); 998 } else { 999 $types = array( 'variables', 'styles', 'presets' ); 1000 } 1001 } 1002 1003 $blocks_metadata = static::get_blocks_metadata(); 1004 $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); 1005 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); 1006 1007 $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); 1008 $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); 1009 1010 if ( ! empty( $options['scope'] ) ) { 1011 foreach ( $setting_nodes as &$node ) { 1012 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); 1013 } 1014 foreach ( $style_nodes as &$node ) { 1015 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); 1016 } 1017 } 1018 1019 if ( ! empty( $options['root_selector'] ) ) { 1020 if ( false !== $root_settings_key ) { 1021 $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; 1022 } 1023 if ( false !== $root_style_key ) { 1024 $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector']; 1025 } 1026 } 1027 1028 $stylesheet = ''; 1029 1030 if ( in_array( 'variables', $types, true ) ) { 1031 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); 1032 } 1033 1034 if ( in_array( 'styles', $types, true ) ) { 1035 if ( false !== $root_style_key ) { 1036 $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); 1037 } 1038 $stylesheet .= $this->get_block_classes( $style_nodes ); 1039 } elseif ( in_array( 'base-layout-styles', $types, true ) ) { 1040 $root_selector = static::ROOT_BLOCK_SELECTOR; 1041 $columns_selector = '.wp-block-columns'; 1042 if ( ! empty( $options['scope'] ) ) { 1043 $root_selector = static::scope_selector( $options['scope'], $root_selector ); 1044 $columns_selector = static::scope_selector( $options['scope'], $columns_selector ); 1045 } 1046 if ( ! empty( $options['root_selector'] ) ) { 1047 $root_selector = $options['root_selector']; 1048 } 1049 // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested. 1050 // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value. 1051 $base_styles_nodes = array( 1052 array( 1053 'path' => array( 'styles' ), 1054 'selector' => $root_selector, 1055 ), 1056 array( 1057 'path' => array( 'styles', 'blocks', 'core/columns' ), 1058 'selector' => $columns_selector, 1059 'name' => 'core/columns', 1060 ), 1061 ); 1062 1063 foreach ( $base_styles_nodes as $base_style_node ) { 1064 $stylesheet .= $this->get_layout_styles( $base_style_node ); 1065 } 1066 } 1067 1068 if ( in_array( 'presets', $types, true ) ) { 1069 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); 1070 } 1071 1072 return $stylesheet; 1073 } 1074 1075 /** 1076 * Processes the CSS, to apply nesting. 1077 * 1078 * @since 6.2.0 1079 * 1080 * @param string $css The CSS to process. 1081 * @param string $selector The selector to nest. 1082 * @return string The processed CSS. 1083 */ 1084 protected function process_blocks_custom_css( $css, $selector ) { 1085 $processed_css = ''; 1086 1087 // Split CSS nested rules. 1088 $parts = explode( '&', $css ); 1089 foreach ( $parts as $part ) { 1090 $processed_css .= ( ! str_contains( $part, '{' ) ) 1091 ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level. 1092 : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character. 1093 } 1094 return $processed_css; 1095 } 1096 1097 /** 1098 * Returns the global styles custom CSS. 1099 * 1100 * @since 6.2.0 1101 * 1102 * @return string The global styles custom CSS. 1103 */ 1104 public function get_custom_css() { 1105 // Add the global styles root CSS. 1106 $stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' ); 1107 1108 // Add the global styles block CSS. 1109 if ( isset( $this->theme_json['styles']['blocks'] ) ) { 1110 foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) { 1111 $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) ); 1112 if ( $custom_block_css ) { 1113 $selector = static::$blocks_metadata[ $name ]['selector']; 1114 $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector ); 1115 } 1116 } 1117 } 1118 1119 return $stylesheet; 1120 } 1121 1122 /** 1123 * Returns the page templates of the active theme. 1124 * 1125 * @since 5.9.0 1126 * 1127 * @return array 1128 */ 1129 public function get_custom_templates() { 1130 $custom_templates = array(); 1131 if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { 1132 return $custom_templates; 1133 } 1134 1135 foreach ( $this->theme_json['customTemplates'] as $item ) { 1136 if ( isset( $item['name'] ) ) { 1137 $custom_templates[ $item['name'] ] = array( 1138 'title' => isset( $item['title'] ) ? $item['title'] : '', 1139 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), 1140 ); 1141 } 1142 } 1143 return $custom_templates; 1144 } 1145 1146 /** 1147 * Returns the template part data of active theme. 1148 * 1149 * @since 5.9.0 1150 * 1151 * @return array 1152 */ 1153 public function get_template_parts() { 1154 $template_parts = array(); 1155 if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { 1156 return $template_parts; 1157 } 1158 1159 foreach ( $this->theme_json['templateParts'] as $item ) { 1160 if ( isset( $item['name'] ) ) { 1161 $template_parts[ $item['name'] ] = array( 1162 'title' => isset( $item['title'] ) ? $item['title'] : '', 1163 'area' => isset( $item['area'] ) ? $item['area'] : '', 1164 ); 1165 } 1166 } 1167 return $template_parts; 1168 } 1169 1170 /** 1171 * Converts each style section into a list of rulesets 1172 * containing the block styles to be appended to the stylesheet. 1173 * 1174 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax 1175 * 1176 * For each section this creates a new ruleset such as: 1177 * 1178 * block-selector { 1179 * style-property-one: value; 1180 * } 1181 * 1182 * @since 5.8.0 As `get_block_styles()`. 1183 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()` 1184 * and no longer returns preset classes. 1185 * Removed the `$setting_nodes` parameter. 1186 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`. 1187 * 1188 * @param array $style_nodes Nodes with styles. 1189 * @return string The new stylesheet. 1190 */ 1191 protected function get_block_classes( $style_nodes ) { 1192 $block_rules = ''; 1193 1194 foreach ( $style_nodes as $metadata ) { 1195 if ( null === $metadata['selector'] ) { 1196 continue; 1197 } 1198 $block_rules .= static::get_styles_for_block( $metadata ); 1199 } 1200 1201 return $block_rules; 1202 } 1203 1204 /** 1205 * Gets the CSS layout rules for a particular block from theme.json layout definitions. 1206 * 1207 * @since 6.1.0 1208 * 1209 * @param array $block_metadata Metadata about the block to get styles for. 1210 * @return string Layout styles for the block. 1211 */ 1212 protected function get_layout_styles( $block_metadata ) { 1213 $block_rules = ''; 1214 $block_type = null; 1215 1216 // Skip outputting layout styles if explicitly disabled. 1217 if ( current_theme_supports( 'disable-layout-styles' ) ) { 1218 return $block_rules; 1219 } 1220 1221 if ( isset( $block_metadata['name'] ) ) { 1222 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); 1223 if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) { 1224 return $block_rules; 1225 } 1226 } 1227 1228 $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : ''; 1229 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; 1230 $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support. 1231 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); 1232 $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() ); 1233 $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. 1234 1235 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap. 1236 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. 1237 if ( $has_block_gap_support || $has_fallback_gap_support ) { 1238 $block_gap_value = null; 1239 // Use a fallback gap value if block gap support is not available. 1240 if ( ! $has_block_gap_support ) { 1241 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; 1242 if ( ! empty( $block_type ) ) { 1243 $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null ); 1244 } 1245 } else { 1246 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); 1247 } 1248 1249 // Support split row / column values and concatenate to a shorthand value. 1250 if ( is_array( $block_gap_value ) ) { 1251 if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { 1252 $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); 1253 $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); 1254 $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; 1255 } else { 1256 // Skip outputting gap value if not all sides are provided. 1257 $block_gap_value = null; 1258 } 1259 } 1260 1261 // If the block should have custom gap, add the gap styles. 1262 if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { 1263 foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { 1264 // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. 1265 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) { 1266 continue; 1267 } 1268 1269 $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); 1270 $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() ); 1271 1272 if ( 1273 ! empty( $class_name ) && 1274 ! empty( $spacing_rules ) 1275 ) { 1276 foreach ( $spacing_rules as $spacing_rule ) { 1277 $declarations = array(); 1278 if ( 1279 isset( $spacing_rule['selector'] ) && 1280 preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && 1281 ! empty( $spacing_rule['rules'] ) 1282 ) { 1283 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. 1284 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { 1285 $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; 1286 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { 1287 $declarations[] = array( 1288 'name' => $css_property, 1289 'value' => $current_css_value, 1290 ); 1291 } 1292 } 1293 1294 if ( ! $has_block_gap_support ) { 1295 // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. 1296 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; 1297 $layout_selector = sprintf( 1298 $format, 1299 $selector, 1300 $class_name, 1301 $spacing_rule['selector'] 1302 ); 1303 } else { 1304 $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s'; 1305 $layout_selector = sprintf( 1306 $format, 1307 $selector, 1308 $class_name, 1309 $spacing_rule['selector'] 1310 ); 1311 } 1312 $block_rules .= static::to_ruleset( $layout_selector, $declarations ); 1313 } 1314 } 1315 } 1316 } 1317 } 1318 } 1319 1320 // Output base styles. 1321 if ( 1322 static::ROOT_BLOCK_SELECTOR === $selector 1323 ) { 1324 $valid_display_modes = array( 'block', 'flex', 'grid' ); 1325 foreach ( $layout_definitions as $layout_definition ) { 1326 $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); 1327 $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() ); 1328 1329 if ( 1330 ! empty( $class_name ) && 1331 ! empty( $base_style_rules ) 1332 ) { 1333 // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. 1334 if ( 1335 ! empty( $layout_definition['displayMode'] ) && 1336 is_string( $layout_definition['displayMode'] ) && 1337 in_array( $layout_definition['displayMode'], $valid_display_modes, true ) 1338 ) { 1339 $layout_selector = sprintf( 1340 '%s .%s', 1341 $selector, 1342 $class_name 1343 ); 1344 $block_rules .= static::to_ruleset( 1345 $layout_selector, 1346 array( 1347 array( 1348 'name' => 'display', 1349 'value' => $layout_definition['displayMode'], 1350 ), 1351 ) 1352 ); 1353 } 1354 1355 foreach ( $base_style_rules as $base_style_rule ) { 1356 $declarations = array(); 1357 1358 if ( 1359 isset( $base_style_rule['selector'] ) && 1360 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && 1361 ! empty( $base_style_rule['rules'] ) 1362 ) { 1363 foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { 1364 if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { 1365 $declarations[] = array( 1366 'name' => $css_property, 1367 'value' => $css_value, 1368 ); 1369 } 1370 } 1371 1372 $layout_selector = sprintf( 1373 '%s .%s%s', 1374 $selector, 1375 $class_name, 1376 $base_style_rule['selector'] 1377 ); 1378 $block_rules .= static::to_ruleset( $layout_selector, $declarations ); 1379 } 1380 } 1381 } 1382 } 1383 } 1384 return $block_rules; 1385 } 1386 1387 /** 1388 * Creates new rulesets as classes for each preset value such as: 1389 * 1390 * .has-value-color { 1391 * color: value; 1392 * } 1393 * 1394 * .has-value-background-color { 1395 * background-color: value; 1396 * } 1397 * 1398 * .has-value-font-size { 1399 * font-size: value; 1400 * } 1401 * 1402 * .has-value-gradient-background { 1403 * background: value; 1404 * } 1405 * 1406 * p.has-value-gradient-background { 1407 * background: value; 1408 * } 1409 * 1410 * @since 5.9.0 1411 * 1412 * @param array $setting_nodes Nodes with settings. 1413 * @param string[] $origins List of origins to process presets from. 1414 * @return string The new stylesheet. 1415 */ 1416 protected function get_preset_classes( $setting_nodes, $origins ) { 1417 $preset_rules = ''; 1418 1419 foreach ( $setting_nodes as $metadata ) { 1420 if ( null === $metadata['selector'] ) { 1421 continue; 1422 } 1423 1424 $selector = $metadata['selector']; 1425 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 1426 $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); 1427 } 1428 1429 return $preset_rules; 1430 } 1431 1432 /** 1433 * Converts each styles section into a list of rulesets 1434 * to be appended to the stylesheet. 1435 * These rulesets contain all the css variables (custom variables and preset variables). 1436 * 1437 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax 1438 * 1439 * For each section this creates a new ruleset such as: 1440 * 1441 * block-selector { 1442 * --wp--preset--category--slug: value; 1443 * --wp--custom--variable: value; 1444 * } 1445 * 1446 * @since 5.8.0 1447 * @since 5.9.0 Added the `$origins` parameter. 1448 * 1449 * @param array $nodes Nodes with settings. 1450 * @param string[] $origins List of origins to process. 1451 * @return string The new stylesheet. 1452 */ 1453 protected function get_css_variables( $nodes, $origins ) { 1454 $stylesheet = ''; 1455 foreach ( $nodes as $metadata ) { 1456 if ( null === $metadata['selector'] ) { 1457 continue; 1458 } 1459 1460 $selector = $metadata['selector']; 1461 1462 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 1463 $declarations = static::compute_preset_vars( $node, $origins ); 1464 $theme_vars_declarations = static::compute_theme_vars( $node ); 1465 foreach ( $theme_vars_declarations as $theme_vars_declaration ) { 1466 $declarations[] = $theme_vars_declaration; 1467 } 1468 1469 $stylesheet .= static::to_ruleset( $selector, $declarations ); 1470 } 1471 1472 return $stylesheet; 1473 } 1474 1475 /** 1476 * Given a selector and a declaration list, 1477 * creates the corresponding ruleset. 1478 * 1479 * @since 5.8.0 1480 * 1481 * @param string $selector CSS selector. 1482 * @param array $declarations List of declarations. 1483 * @return string The resulting CSS ruleset. 1484 */ 1485 protected static function to_ruleset( $selector, $declarations ) { 1486 if ( empty( $declarations ) ) { 1487 return ''; 1488 } 1489 1490 $declaration_block = array_reduce( 1491 $declarations, 1492 static function ( $carry, $element ) { 1493 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, 1494 '' 1495 ); 1496 1497 return $selector . '{' . $declaration_block . '}'; 1498 } 1499 1500 /** 1501 * Given a settings array, returns the generated rulesets 1502 * for the preset classes. 1503 * 1504 * @since 5.8.0 1505 * @since 5.9.0 Added the `$origins` parameter. 1506 * 1507 * @param array $settings Settings to process. 1508 * @param string $selector Selector wrapping the classes. 1509 * @param string[] $origins List of origins to process. 1510 * @return string The result of processing the presets. 1511 */ 1512 protected static function compute_preset_classes( $settings, $selector, $origins ) { 1513 if ( static::ROOT_BLOCK_SELECTOR === $selector ) { 1514 // Classes at the global level do not need any CSS prefixed, 1515 // and we don't want to increase its specificity. 1516 $selector = ''; 1517 } 1518 1519 $stylesheet = ''; 1520 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 1521 $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); 1522 foreach ( $preset_metadata['classes'] as $class => $property ) { 1523 foreach ( $slugs as $slug ) { 1524 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); 1525 $class_name = static::replace_slug_in_string( $class, $slug ); 1526 $stylesheet .= static::to_ruleset( 1527 static::append_to_selector( $selector, $class_name ), 1528 array( 1529 array( 1530 'name' => $property, 1531 'value' => 'var(' . $css_var . ') !important', 1532 ), 1533 ) 1534 ); 1535 } 1536 } 1537 } 1538 1539 return $stylesheet; 1540 } 1541 1542 /** 1543 * Function that scopes a selector with another one. This works a bit like 1544 * SCSS nesting except the `&` operator isn't supported. 1545 * 1546 * <code> 1547 * $scope = '.a, .b .c'; 1548 * $selector = '> .x, .y'; 1549 * $merged = scope_selector( $scope, $selector ); 1550 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' 1551 * </code> 1552 * 1553 * @since 5.9.0 1554 * 1555 * @param string $scope Selector to scope to. 1556 * @param string $selector Original selector. 1557 * @return string Scoped selector. 1558 */ 1559 public static function scope_selector( $scope, $selector ) { 1560 $scopes = explode( ',', $scope ); 1561 $selectors = explode( ',', $selector ); 1562 1563 $selectors_scoped = array(); 1564 foreach ( $scopes as $outer ) { 1565 foreach ( $selectors as $inner ) { 1566 $outer = trim( $outer ); 1567 $inner = trim( $inner ); 1568 if ( ! empty( $outer ) && ! empty( $inner ) ) { 1569 $selectors_scoped[] = $outer . ' ' . $inner; 1570 } elseif ( empty( $outer ) ) { 1571 $selectors_scoped[] = $inner; 1572 } elseif ( empty( $inner ) ) { 1573 $selectors_scoped[] = $outer; 1574 } 1575 } 1576 } 1577 1578 $result = implode( ', ', $selectors_scoped ); 1579 return $result; 1580 } 1581 1582 /** 1583 * Gets preset values keyed by slugs based on settings and metadata. 1584 * 1585 * <code> 1586 * $settings = array( 1587 * 'typography' => array( 1588 * 'fontFamilies' => array( 1589 * array( 1590 * 'slug' => 'sansSerif', 1591 * 'fontFamily' => '"Helvetica Neue", sans-serif', 1592 * ), 1593 * array( 1594 * 'slug' => 'serif', 1595 * 'colors' => 'Georgia, serif', 1596 * ) 1597 * ), 1598 * ), 1599 * ); 1600 * $meta = array( 1601 * 'path' => array( 'typography', 'fontFamilies' ), 1602 * 'value_key' => 'fontFamily', 1603 * ); 1604 * $values_by_slug = get_settings_values_by_slug(); 1605 * // $values_by_slug === array( 1606 * // 'sans-serif' => '"Helvetica Neue", sans-serif', 1607 * // 'serif' => 'Georgia, serif', 1608 * // ); 1609 * </code> 1610 * 1611 * @since 5.9.0 1612 * 1613 * @param array $settings Settings to process. 1614 * @param array $preset_metadata One of the PRESETS_METADATA values. 1615 * @param string[] $origins List of origins to process. 1616 * @return array Array of presets where each key is a slug and each value is the preset value. 1617 */ 1618 protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { 1619 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 1620 1621 $result = array(); 1622 foreach ( $origins as $origin ) { 1623 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 1624 continue; 1625 } 1626 foreach ( $preset_per_origin[ $origin ] as $preset ) { 1627 $slug = _wp_to_kebab_case( $preset['slug'] ); 1628 1629 $value = ''; 1630 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { 1631 $value_key = $preset_metadata['value_key']; 1632 $value = $preset[ $value_key ]; 1633 } elseif ( 1634 isset( $preset_metadata['value_func'] ) && 1635 is_callable( $preset_metadata['value_func'] ) 1636 ) { 1637 $value_func = $preset_metadata['value_func']; 1638 $value = call_user_func( $value_func, $preset ); 1639 } else { 1640 // If we don't have a value, then don't add it to the result. 1641 continue; 1642 } 1643 1644 $result[ $slug ] = $value; 1645 } 1646 } 1647 return $result; 1648 } 1649 1650 /** 1651 * Similar to get_settings_values_by_slug, but doesn't compute the value. 1652 * 1653 * @since 5.9.0 1654 * 1655 * @param array $settings Settings to process. 1656 * @param array $preset_metadata One of the PRESETS_METADATA values. 1657 * @param string[] $origins List of origins to process. 1658 * @return array Array of presets where the key and value are both the slug. 1659 */ 1660 protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { 1661 if ( null === $origins ) { 1662 $origins = static::VALID_ORIGINS; 1663 } 1664 1665 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 1666 1667 $result = array(); 1668 foreach ( $origins as $origin ) { 1669 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 1670 continue; 1671 } 1672 foreach ( $preset_per_origin[ $origin ] as $preset ) { 1673 $slug = _wp_to_kebab_case( $preset['slug'] ); 1674 1675 // Use the array as a set so we don't get duplicates. 1676 $result[ $slug ] = $slug; 1677 } 1678 } 1679 return $result; 1680 } 1681 1682 /** 1683 * Transforms a slug into a CSS Custom Property. 1684 * 1685 * @since 5.9.0 1686 * 1687 * @param string $input String to replace. 1688 * @param string $slug The slug value to use to generate the custom property. 1689 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. 1690 */ 1691 protected static function replace_slug_in_string( $input, $slug ) { 1692 return strtr( $input, array( '$slug' => $slug ) ); 1693 } 1694 1695 /** 1696 * Given the block settings, extracts the CSS Custom Properties 1697 * for the presets and adds them to the $declarations array 1698 * following the format: 1699 * 1700 * array( 1701 * 'name' => 'property_name', 1702 * 'value' => 'property_value, 1703 * ) 1704 * 1705 * @since 5.8.0 1706 * @since 5.9.0 Added the `$origins` parameter. 1707 * 1708 * @param array $settings Settings to process. 1709 * @param string[] $origins List of origins to process. 1710 * @return array The modified $declarations. 1711 */ 1712 protected static function compute_preset_vars( $settings, $origins ) { 1713 $declarations = array(); 1714 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 1715 $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); 1716 foreach ( $values_by_slug as $slug => $value ) { 1717 $declarations[] = array( 1718 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), 1719 'value' => $value, 1720 ); 1721 } 1722 } 1723 1724 return $declarations; 1725 } 1726 1727 /** 1728 * Given an array of settings, extracts the CSS Custom Properties 1729 * for the custom values and adds them to the $declarations 1730 * array following the format: 1731 * 1732 * array( 1733 * 'name' => 'property_name', 1734 * 'value' => 'property_value, 1735 * ) 1736 * 1737 * @since 5.8.0 1738 * 1739 * @param array $settings Settings to process. 1740 * @return array The modified $declarations. 1741 */ 1742 protected static function compute_theme_vars( $settings ) { 1743 $declarations = array(); 1744 $custom_values = _wp_array_get( $settings, array( 'custom' ), array() ); 1745 $css_vars = static::flatten_tree( $custom_values ); 1746 foreach ( $css_vars as $key => $value ) { 1747 $declarations[] = array( 1748 'name' => '--wp--custom--' . $key, 1749 'value' => $value, 1750 ); 1751 } 1752 1753 return $declarations; 1754 } 1755 1756 /** 1757 * Given a tree, it creates a flattened one 1758 * by merging the keys and binding the leaf values 1759 * to the new keys. 1760 * 1761 * It also transforms camelCase names into kebab-case 1762 * and substitutes '/' by '-'. 1763 * 1764 * This is thought to be useful to generate 1765 * CSS Custom Properties from a tree, 1766 * although there's nothing in the implementation 1767 * of this function that requires that format. 1768 * 1769 * For example, assuming the given prefix is '--wp' 1770 * and the token is '--', for this input tree: 1771 * 1772 * { 1773 * 'some/property': 'value', 1774 * 'nestedProperty': { 1775 * 'sub-property': 'value' 1776 * } 1777 * } 1778 * 1779 * it'll return this output: 1780 * 1781 * { 1782 * '--wp--some-property': 'value', 1783 * '--wp--nested-property--sub-property': 'value' 1784 * } 1785 * 1786 * @since 5.8.0 1787 * 1788 * @param array $tree Input tree to process. 1789 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. 1790 * @param string $token Optional. Token to use between levels. Default '--'. 1791 * @return array The flattened tree. 1792 */ 1793 protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { 1794 $result = array(); 1795 foreach ( $tree as $property => $value ) { 1796 $new_key = $prefix . str_replace( 1797 '/', 1798 '-', 1799 strtolower( _wp_to_kebab_case( $property ) ) 1800 ); 1801 1802 if ( is_array( $value ) ) { 1803 $new_prefix = $new_key . $token; 1804 $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token ); 1805 foreach ( $flattened_subtree as $subtree_key => $subtree_value ) { 1806 $result[ $subtree_key ] = $subtree_value; 1807 } 1808 } else { 1809 $result[ $new_key ] = $value; 1810 } 1811 } 1812 return $result; 1813 } 1814 1815 /** 1816 * Given a styles array, it extracts the style properties 1817 * and adds them to the $declarations array following the format: 1818 * 1819 * array( 1820 * 'name' => 'property_name', 1821 * 'value' => 'property_value, 1822 * ) 1823 * 1824 * @since 5.8.0 1825 * @since 5.9.0 Added the `$settings` and `$properties` parameters. 1826 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters. 1827 * 1828 * @param array $styles Styles to process. 1829 * @param array $settings Theme settings. 1830 * @param array $properties Properties metadata. 1831 * @param array $theme_json Theme JSON array. 1832 * @param string $selector The style block selector. 1833 * @param boolean $use_root_padding Whether to add custom properties at root level. 1834 * @return array Returns the modified $declarations. 1835 */ 1836 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { 1837 if ( null === $properties ) { 1838 $properties = static::PROPERTIES_METADATA; 1839 } 1840 1841 $declarations = array(); 1842 if ( empty( $styles ) ) { 1843 return $declarations; 1844 } 1845 1846 $root_variable_duplicates = array(); 1847 1848 foreach ( $properties as $css_property => $value_path ) { 1849 $value = static::get_property_value( $styles, $value_path, $theme_json ); 1850 1851 if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { 1852 continue; 1853 } 1854 // Root-level padding styles don't currently support strings with CSS shorthand values. 1855 // This may change: https://github.com/WordPress/gutenberg/issues/40132. 1856 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { 1857 continue; 1858 } 1859 1860 if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { 1861 $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); 1862 } 1863 1864 // Look up protected properties, keyed by value path. 1865 // Skip protected properties that are explicitly set to `null`. 1866 if ( is_array( $value_path ) ) { 1867 $path_string = implode( '.', $value_path ); 1868 if ( 1869 // TODO: Replace array_key_exists() with isset() check once WordPress drops 1870 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 1871 array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && 1872 _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null 1873 ) { 1874 continue; 1875 } 1876 } 1877 1878 // Skip if empty and not "0" or value represents array of longhand values. 1879 $has_missing_value = empty( $value ) && ! is_numeric( $value ); 1880 if ( $has_missing_value || is_array( $value ) ) { 1881 continue; 1882 } 1883 1884 // Calculates fluid typography rules where available. 1885 if ( 'font-size' === $css_property ) { 1886 /* 1887 * wp_get_typography_font_size_value() will check 1888 * if fluid typography has been activated and also 1889 * whether the incoming value can be converted to a fluid value. 1890 * Values that already have a clamp() function will not pass the test, 1891 * and therefore the original $value will be returned. 1892 */ 1893 $value = wp_get_typography_font_size_value( array( 'size' => $value ) ); 1894 } 1895 1896 $declarations[] = array( 1897 'name' => $css_property, 1898 'value' => $value, 1899 ); 1900 } 1901 1902 // If a variable value is added to the root, the corresponding property should be removed. 1903 foreach ( $root_variable_duplicates as $duplicate ) { 1904 $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); 1905 if ( is_numeric( $discard ) ) { 1906 array_splice( $declarations, $discard, 1 ); 1907 } 1908 } 1909 1910 return $declarations; 1911 } 1912 1913 /** 1914 * Returns the style property for the given path. 1915 * 1916 * It also converts CSS Custom Property stored as 1917 * "var:preset|color|secondary" to the form 1918 * "--wp--preset--color--secondary". 1919 * 1920 * It also converts references to a path to the value 1921 * stored at that location, e.g. 1922 * { "ref": "style.color.background" } => "#fff". 1923 * 1924 * @since 5.8.0 1925 * @since 5.9.0 Added support for values of array type, which are returned as is. 1926 * @since 6.1.0 Added the `$theme_json` parameter. 1927 * 1928 * @param array $styles Styles subtree. 1929 * @param array $path Which property to process. 1930 * @param array $theme_json Theme JSON array. 1931 * @return string|array Style property value. 1932 */ 1933 protected static function get_property_value( $styles, $path, $theme_json = null ) { 1934 $value = _wp_array_get( $styles, $path, '' ); 1935 1936 if ( '' === $value || null === $value ) { 1937 // No need to process the value further. 1938 return ''; 1939 } 1940 1941 /* 1942 * This converts references to a path to the value at that path 1943 * where the values is an array with a "ref" key, pointing to a path. 1944 * For example: { "ref": "style.color.background" } => "#fff". 1945 */ 1946 if ( is_array( $value ) && isset( $value['ref'] ) ) { 1947 $value_path = explode( '.', $value['ref'] ); 1948 $ref_value = _wp_array_get( $theme_json, $value_path ); 1949 // Only use the ref value if we find anything. 1950 if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { 1951 $value = $ref_value; 1952 } 1953 1954 if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) { 1955 $path_string = json_encode( $path ); 1956 $ref_value_string = json_encode( $ref_value ); 1957 _doing_it_wrong( 1958 'get_property_value', 1959 sprintf( 1960 /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ 1961 __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ), 1962 'theme.json', 1963 $ref_value_string, 1964 $path_string, 1965 $ref_value['ref'] 1966 ), 1967 '6.1.0' 1968 ); 1969 } 1970 } 1971 1972 if ( is_array( $value ) ) { 1973 return $value; 1974 } 1975 1976 // Convert custom CSS properties. 1977 $prefix = 'var:'; 1978 $prefix_len = strlen( $prefix ); 1979 $token_in = '|'; 1980 $token_out = '--'; 1981 if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { 1982 $unwrapped_name = str_replace( 1983 $token_in, 1984 $token_out, 1985 substr( $value, $prefix_len ) 1986 ); 1987 $value = "var(--wp--$unwrapped_name)"; 1988 } 1989 1990 return $value; 1991 } 1992 1993 /** 1994 * Builds metadata for the setting nodes, which returns in the form of: 1995 * 1996 * [ 1997 * [ 1998 * 'path' => ['path', 'to', 'some', 'node' ], 1999 * 'selector' => 'CSS selector for some node' 2000 * ], 2001 * [ 2002 * 'path' => [ 'path', 'to', 'other', 'node' ], 2003 * 'selector' => 'CSS selector for other node' 2004 * ], 2005 * ] 2006 * 2007 * @since 5.8.0 2008 * 2009 * @param array $theme_json The tree to extract setting nodes from. 2010 * @param array $selectors List of selectors per block. 2011 * @return array An array of setting nodes metadata. 2012 */ 2013 protected static function get_setting_nodes( $theme_json, $selectors = array() ) { 2014 $nodes = array(); 2015 if ( ! isset( $theme_json['settings'] ) ) { 2016 return $nodes; 2017 } 2018 2019 // Top-level. 2020 $nodes[] = array( 2021 'path' => array( 'settings' ), 2022 'selector' => static::ROOT_BLOCK_SELECTOR, 2023 ); 2024 2025 // Calculate paths for blocks. 2026 if ( ! isset( $theme_json['settings']['blocks'] ) ) { 2027 return $nodes; 2028 } 2029 2030 foreach ( $theme_json['settings']['blocks'] as $name => $node ) { 2031 $selector = null; 2032 if ( isset( $selectors[ $name ]['selector'] ) ) { 2033 $selector = $selectors[ $name ]['selector']; 2034 } 2035 2036 $nodes[] = array( 2037 'path' => array( 'settings', 'blocks', $name ), 2038 'selector' => $selector, 2039 ); 2040 } 2041 2042 return $nodes; 2043 } 2044 2045 /** 2046 * Builds metadata for the style nodes, which returns in the form of: 2047 * 2048 * [ 2049 * [ 2050 * 'path' => [ 'path', 'to', 'some', 'node' ], 2051 * 'selector' => 'CSS selector for some node', 2052 * 'duotone' => 'CSS selector for duotone for some node' 2053 * ], 2054 * [ 2055 * 'path' => ['path', 'to', 'other', 'node' ], 2056 * 'selector' => 'CSS selector for other node', 2057 * 'duotone' => null 2058 * ], 2059 * ] 2060 * 2061 * @since 5.8.0 2062 * 2063 * @param array $theme_json The tree to extract style nodes from. 2064 * @param array $selectors List of selectors per block. 2065 * @return array An array of style nodes metadata. 2066 */ 2067 protected static function get_style_nodes( $theme_json, $selectors = array() ) { 2068 $nodes = array(); 2069 if ( ! isset( $theme_json['styles'] ) ) { 2070 return $nodes; 2071 } 2072 2073 // Top-level. 2074 $nodes[] = array( 2075 'path' => array( 'styles' ), 2076 'selector' => static::ROOT_BLOCK_SELECTOR, 2077 ); 2078 2079 if ( isset( $theme_json['styles']['elements'] ) ) { 2080 foreach ( self::ELEMENTS as $element => $selector ) { 2081 if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) { 2082 continue; 2083 } 2084 $nodes[] = array( 2085 'path' => array( 'styles', 'elements', $element ), 2086 'selector' => static::ELEMENTS[ $element ], 2087 ); 2088 2089 // Handle any pseudo selectors for the element. 2090 // TODO: Replace array_key_exists() with isset() check once WordPress drops 2091 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 2092 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { 2093 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 2094 2095 if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { 2096 $nodes[] = array( 2097 'path' => array( 'styles', 'elements', $element ), 2098 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), 2099 ); 2100 } 2101 } 2102 } 2103 } 2104 } 2105 2106 // Blocks. 2107 if ( ! isset( $theme_json['styles']['blocks'] ) ) { 2108 return $nodes; 2109 } 2110 2111 $block_nodes = static::get_block_nodes( $theme_json ); 2112 foreach ( $block_nodes as $block_node ) { 2113 $nodes[] = $block_node; 2114 } 2115 2116 /** 2117 * Filters the list of style nodes with metadata. 2118 * 2119 * This allows for things like loading block CSS independently. 2120 * 2121 * @since 6.1.0 2122 * 2123 * @param array $nodes Style nodes with metadata. 2124 */ 2125 return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); 2126 } 2127 2128 /** 2129 * A public helper to get the block nodes from a theme.json file. 2130 * 2131 * @since 6.1.0 2132 * 2133 * @return array The block nodes in theme.json. 2134 */ 2135 public function get_styles_block_nodes() { 2136 return static::get_block_nodes( $this->theme_json ); 2137 } 2138 2139 /** 2140 * Returns a filtered declarations array if there is a separator block with only a background 2141 * style defined in theme.json by adding a color attribute to reflect the changes in the front. 2142 * 2143 * @since 6.1.1 2144 * 2145 * @param array $declarations List of declarations. 2146 * @return array $declarations List of declarations filtered. 2147 */ 2148 private static function update_separator_declarations( $declarations ) { 2149 $background_color = ''; 2150 $border_color_matches = false; 2151 $text_color_matches = false; 2152 2153 foreach ( $declarations as $declaration ) { 2154 if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) { 2155 $background_color = $declaration['value']; 2156 } elseif ( 'border-color' === $declaration['name'] ) { 2157 $border_color_matches = true; 2158 } elseif ( 'color' === $declaration['name'] ) { 2159 $text_color_matches = true; 2160 } 2161 2162 if ( $background_color && $border_color_matches && $text_color_matches ) { 2163 break; 2164 } 2165 } 2166 2167 if ( $background_color && ! $border_color_matches && ! $text_color_matches ) { 2168 $declarations[] = array( 2169 'name' => 'color', 2170 'value' => $background_color, 2171 ); 2172 } 2173 2174 return $declarations; 2175 } 2176 2177 /** 2178 * An internal method to get the block nodes from a theme.json file. 2179 * 2180 * @since 6.1.0 2181 * 2182 * @param array $theme_json The theme.json converted to an array. 2183 * @return array The block nodes in theme.json. 2184 */ 2185 private static function get_block_nodes( $theme_json ) { 2186 $selectors = static::get_blocks_metadata(); 2187 $nodes = array(); 2188 if ( ! isset( $theme_json['styles'] ) ) { 2189 return $nodes; 2190 } 2191 2192 // Blocks. 2193 if ( ! isset( $theme_json['styles']['blocks'] ) ) { 2194 return $nodes; 2195 } 2196 2197 foreach ( $theme_json['styles']['blocks'] as $name => $node ) { 2198 $selector = null; 2199 if ( isset( $selectors[ $name ]['selector'] ) ) { 2200 $selector = $selectors[ $name ]['selector']; 2201 } 2202 2203 $duotone_selector = null; 2204 if ( isset( $selectors[ $name ]['duotone'] ) ) { 2205 $duotone_selector = $selectors[ $name ]['duotone']; 2206 } 2207 2208 $feature_selectors = null; 2209 if ( isset( $selectors[ $name ]['features'] ) ) { 2210 $feature_selectors = $selectors[ $name ]['features']; 2211 } 2212 2213 $variation_selectors = array(); 2214 if ( isset( $node['variations'] ) ) { 2215 foreach ( $node['variations'] as $variation => $node ) { 2216 $variation_selectors[] = array( 2217 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), 2218 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], 2219 ); 2220 } 2221 } 2222 2223 $nodes[] = array( 2224 'name' => $name, 2225 'path' => array( 'styles', 'blocks', $name ), 2226 'selector' => $selector, 2227 'duotone' => $duotone_selector, 2228 'features' => $feature_selectors, 2229 'variations' => $variation_selectors, 2230 ); 2231 2232 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { 2233 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { 2234 $nodes[] = array( 2235 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 2236 'selector' => $selectors[ $name ]['elements'][ $element ], 2237 ); 2238 2239 // Handle any pseudo selectors for the element. 2240 // TODO: Replace array_key_exists() with isset() check once WordPress drops 2241 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 2242 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { 2243 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 2244 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { 2245 $nodes[] = array( 2246 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 2247 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), 2248 ); 2249 } 2250 } 2251 } 2252 } 2253 } 2254 } 2255 2256 return $nodes; 2257 } 2258 2259 /** 2260 * Gets the CSS rules for a particular block from theme.json. 2261 * 2262 * @since 6.1.0 2263 * 2264 * @param array $block_metadata Metadata about the block to get styles for. 2265 * 2266 * @return string Styles for the block. 2267 */ 2268 public function get_styles_for_block( $block_metadata ) { 2269 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); 2270 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; 2271 $selector = $block_metadata['selector']; 2272 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); 2273 2274 /* 2275 * Process style declarations for block support features the current 2276 * block contains selectors for. Values for a feature with a custom 2277 * selector are filtered from the theme.json node before it is 2278 * processed as normal. 2279 */ 2280 $feature_declarations = array(); 2281 2282 if ( ! empty( $block_metadata['features'] ) ) { 2283 foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { 2284 if ( ! empty( $node[ $feature_name ] ) ) { 2285 // Create temporary node containing only the feature data 2286 // to leverage existing `compute_style_properties` function. 2287 $feature = array( $feature_name => $node[ $feature_name ] ); 2288 // Generate the feature's declarations only. 2289 $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); 2290 2291 // Merge new declarations with any that already exist for 2292 // the feature selector. This may occur when multiple block 2293 // support features use the same custom selector. 2294 if ( isset( $feature_declarations[ $feature_selector ] ) ) { 2295 foreach ( $new_feature_declarations as $new_feature_declaration ) { 2296 $feature_declarations[ $feature_selector ][] = $new_feature_declaration; 2297 } 2298 } else { 2299 $feature_declarations[ $feature_selector ] = $new_feature_declarations; 2300 } 2301 2302 // Remove the feature from the block's node now the 2303 // styles will be included under the feature level selector. 2304 unset( $node[ $feature_name ] ); 2305 } 2306 } 2307 } 2308 2309 // If there are style variations, generate the declarations for them, including any feature selectors the block may have. 2310 $style_variation_declarations = array(); 2311 if ( ! empty( $block_metadata['variations'] ) ) { 2312 foreach ( $block_metadata['variations'] as $style_variation ) { 2313 $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); 2314 $style_variation_selector = $style_variation['selector']; 2315 2316 // If the block has feature selectors, generate the declarations for them within the current style variation. 2317 if ( ! empty( $block_metadata['features'] ) ) { 2318 $clean_style_variation_selector = trim( $style_variation_selector ); 2319 foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { 2320 if ( empty( $style_variation_node[ $feature_name ] ) ) { 2321 continue; 2322 } 2323 // Prepend the variation selector to the feature selector. 2324 $split_feature_selectors = explode( ',', $feature_selector ); 2325 $feature_selectors = array_map( 2326 static function( $split_feature_selector ) use ( $clean_style_variation_selector ) { 2327 return $clean_style_variation_selector . trim( $split_feature_selector ); 2328 }, 2329 $split_feature_selectors 2330 ); 2331 $combined_feature_selectors = implode( ',', $feature_selectors ); 2332 2333 // Compute declarations for the feature. 2334 $new_feature_declarations = static::compute_style_properties( array( $feature_name => $style_variation_node[ $feature_name ] ), $settings, null, $this->theme_json ); 2335 2336 /* 2337 * Merge new declarations with any that already exist for 2338 * the feature selector. This may occur when multiple block 2339 * support features use the same custom selector. 2340 */ 2341 if ( isset( $style_variation_declarations[ $combined_feature_selectors ] ) ) { 2342 $style_variation_declarations[ $combined_feature_selectors ] = array_merge( $style_variation_declarations[ $combined_feature_selectors ], $new_feature_declarations ); 2343 } else { 2344 $style_variation_declarations[ $combined_feature_selectors ] = $new_feature_declarations; 2345 } 2346 /* 2347 * Remove the feature from the variation's node now the 2348 * styles will be included under the feature level selector. 2349 */ 2350 unset( $style_variation_node[ $feature_name ] ); 2351 } 2352 } 2353 // Compute declarations for remaining styles not covered by feature level selectors. 2354 $style_variation_declarations[ $style_variation_selector ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); 2355 } 2356 } 2357 /* 2358 * Get a reference to element name from path. 2359 * $block_metadata['path'] = array( 'styles','elements','link' ); 2360 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. 2361 * Skip non-element paths like just ['styles']. 2362 */ 2363 $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); 2364 2365 $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; 2366 2367 $element_pseudo_allowed = array(); 2368 2369 // TODO: Replace array_key_exists() with isset() check once WordPress drops 2370 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 2371 if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { 2372 $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; 2373 } 2374 2375 /* 2376 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). 2377 * This also resets the array keys. 2378 */ 2379 $pseudo_matches = array_values( 2380 array_filter( 2381 $element_pseudo_allowed, 2382 function( $pseudo_selector ) use ( $selector ) { 2383 return str_contains( $selector, $pseudo_selector ); 2384 } 2385 ) 2386 ); 2387 2388 $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; 2389 2390 /* 2391 * If the current selector is a pseudo selector that's defined in the allow list for the current 2392 * element then compute the style properties for it. 2393 * Otherwise just compute the styles for the default selector as normal. 2394 */ 2395 if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && 2396 // TODO: Replace array_key_exists() with isset() check once WordPress drops 2397 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 2398 array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) 2399 && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) 2400 ) { 2401 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); 2402 } else { 2403 $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); 2404 } 2405 2406 $block_rules = ''; 2407 2408 /* 2409 * 1. Separate the declarations that use the general selector 2410 * from the ones using the duotone selector. 2411 */ 2412 $declarations_duotone = array(); 2413 foreach ( $declarations as $index => $declaration ) { 2414 if ( 'filter' === $declaration['name'] ) { 2415 unset( $declarations[ $index ] ); 2416 $declarations_duotone[] = $declaration; 2417 } 2418 } 2419 2420 // Update declarations if there are separators with only background color defined. 2421 if ( '.wp-block-separator' === $selector ) { 2422 $declarations = static::update_separator_declarations( $declarations ); 2423 } 2424 2425 // 2. Generate and append the rules that use the general selector. 2426 $block_rules .= static::to_ruleset( $selector, $declarations ); 2427 2428 // 3. Generate and append the rules that use the duotone selector. 2429 if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { 2430 $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); 2431 $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); 2432 } 2433 2434 // 4. Generate Layout block gap styles. 2435 if ( 2436 static::ROOT_BLOCK_SELECTOR !== $selector && 2437 ! empty( $block_metadata['name'] ) 2438 ) { 2439 $block_rules .= $this->get_layout_styles( $block_metadata ); 2440 } 2441 2442 // 5. Generate and append the feature level rulesets. 2443 foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { 2444 $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); 2445 } 2446 2447 // 6. Generate and append the style variation rulesets. 2448 foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) { 2449 $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations ); 2450 } 2451 2452 return $block_rules; 2453 } 2454 2455 /** 2456 * Outputs the CSS for layout rules on the root. 2457 * 2458 * @since 6.1.0 2459 * 2460 * @param string $selector The root node selector. 2461 * @param array $block_metadata The metadata for the root block. 2462 * @return string The additional root rules CSS. 2463 */ 2464 public function get_root_layout_rules( $selector, $block_metadata ) { 2465 $css = ''; 2466 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); 2467 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; 2468 2469 /* 2470 * Reset default browser margin on the root body element. 2471 * This is set on the root selector **before** generating the ruleset 2472 * from the `theme.json`. This is to ensure that if the `theme.json` declares 2473 * `margin` in its `spacing` declaration for the `body` element then these 2474 * user-generated values take precedence in the CSS cascade. 2475 * @link https://github.com/WordPress/gutenberg/issues/36147. 2476 */ 2477 $css .= 'body { margin: 0;'; 2478 2479 /* 2480 * If there are content and wide widths in theme.json, output them 2481 * as custom properties on the body element so all blocks can use them. 2482 */ 2483 if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { 2484 $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; 2485 $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; 2486 $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; 2487 $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; 2488 $css .= '--wp--style--global--content-size: ' . $content_size . ';'; 2489 $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; 2490 } 2491 2492 $css .= ' }'; 2493 2494 if ( $use_root_padding ) { 2495 // Top and bottom padding are applied to the outer block container. 2496 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; 2497 // Right and left padding are applied to the first container with `.has-global-padding` class. 2498 $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; 2499 // Nested containers with `.has-global-padding` class do not get padding. 2500 $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }'; 2501 // Alignfull children of the container with left and right padding have negative margins so they can still be full width. 2502 $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; 2503 // The above rule is negated for alignfull children of nested containers. 2504 $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; 2505 // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. 2506 $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; 2507 // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. 2508 $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; 2509 } 2510 2511 $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; 2512 $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; 2513 $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; 2514 2515 $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' ); 2516 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; 2517 if ( $has_block_gap_support ) { 2518 $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); 2519 $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; 2520 $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }"; 2521 2522 // For backwards compatibility, ensure the legacy block gap CSS variable is still available. 2523 $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; 2524 } 2525 $css .= $this->get_layout_styles( $block_metadata ); 2526 2527 return $css; 2528 } 2529 2530 /** 2531 * For metadata values that can either be booleans or paths to booleans, gets the value. 2532 * 2533 * $data = array( 2534 * 'color' => array( 2535 * 'defaultPalette' => true 2536 * ) 2537 * ); 2538 * 2539 * static::get_metadata_boolean( $data, false ); 2540 * // => false 2541 * 2542 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); 2543 * // => true 2544 * 2545 * @since 6.0.0 2546 * 2547 * @param array $data The data to inspect. 2548 * @param bool|array $path Boolean or path to a boolean. 2549 * @param bool $default_value Default value if the referenced path is missing. 2550 * Default false. 2551 * @return bool Value of boolean metadata. 2552 */ 2553 protected static function get_metadata_boolean( $data, $path, $default_value = false ) { 2554 if ( is_bool( $path ) ) { 2555 return $path; 2556 } 2557 2558 if ( is_array( $path ) ) { 2559 $value = _wp_array_get( $data, $path ); 2560 if ( null !== $value ) { 2561 return $value; 2562 } 2563 } 2564 2565 return $default_value; 2566 } 2567 2568 /** 2569 * Merges new incoming data. 2570 * 2571 * @since 5.8.0 2572 * @since 5.9.0 Duotone preset also has origins. 2573 * 2574 * @param WP_Theme_JSON $incoming Data to merge. 2575 */ 2576 public function merge( $incoming ) { 2577 $incoming_data = $incoming->get_raw_data(); 2578 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); 2579 2580 /* 2581 * The array_replace_recursive algorithm merges at the leaf level, 2582 * but we don't want leaf arrays to be merged, so we overwrite it. 2583 * 2584 * For leaf values that are sequential arrays it will use the numeric indexes for replacement. 2585 * We rather replace the existing with the incoming value, if it exists. 2586 * This is the case of spacing.units. 2587 * 2588 * For leaf values that are associative arrays it will merge them as expected. 2589 * This is also not the behavior we want for the current associative arrays (presets). 2590 * We rather replace the existing with the incoming value, if it exists. 2591 * This happens, for example, when we merge data from theme.json upon existing 2592 * theme supports or when we merge anything coming from the same source twice. 2593 * This is the case of color.palette, color.gradients, color.duotone, 2594 * typography.fontSizes, or typography.fontFamilies. 2595 * 2596 * Additionally, for some preset types, we also want to make sure the 2597 * values they introduce don't conflict with default values. We do so 2598 * by checking the incoming slugs for theme presets and compare them 2599 * with the equivalent default presets: if a slug is present as a default 2600 * we remove it from the theme presets. 2601 */ 2602 $nodes = static::get_setting_nodes( $incoming_data ); 2603 $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); 2604 foreach ( $nodes as $node ) { 2605 // Replace the spacing.units. 2606 $path = $node['path']; 2607 $path[] = 'spacing'; 2608 $path[] = 'units'; 2609 2610 $content = _wp_array_get( $incoming_data, $path, null ); 2611 if ( isset( $content ) ) { 2612 _wp_array_set( $this->theme_json, $path, $content ); 2613 } 2614 2615 // Replace the presets. 2616 foreach ( static::PRESETS_METADATA as $preset ) { 2617 $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); 2618 2619 foreach ( static::VALID_ORIGINS as $origin ) { 2620 $base_path = $node['path']; 2621 foreach ( $preset['path'] as $leaf ) { 2622 $base_path[] = $leaf; 2623 } 2624 2625 $path = $base_path; 2626 $path[] = $origin; 2627 2628 $content = _wp_array_get( $incoming_data, $path, null ); 2629 if ( ! isset( $content ) ) { 2630 continue; 2631 } 2632 2633 if ( 'theme' === $origin && $preset['use_default_names'] ) { 2634 foreach ( $content as $key => $item ) { 2635 if ( ! isset( $item['name'] ) ) { 2636 $name = static::get_name_from_defaults( $item['slug'], $base_path ); 2637 if ( null !== $name ) { 2638 $content[ $key ]['name'] = $name; 2639 } 2640 } 2641 } 2642 } 2643 2644 if ( 2645 ( 'theme' !== $origin ) || 2646 ( 'theme' === $origin && $override_preset ) 2647 ) { 2648 _wp_array_set( $this->theme_json, $path, $content ); 2649 } else { 2650 $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); 2651 $slugs = array_merge_recursive( $slugs_global, $slugs_node ); 2652 2653 $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); 2654 $content = static::filter_slugs( $content, $slugs_for_preset ); 2655 _wp_array_set( $this->theme_json, $path, $content ); 2656 } 2657 } 2658 } 2659 } 2660 } 2661 2662 /** 2663 * Converts all filter (duotone) presets into SVGs. 2664 * 2665 * @since 5.9.1 2666 * 2667 * @param array $origins List of origins to process. 2668 * @return string SVG filters. 2669 */ 2670 public function get_svg_filters( $origins ) { 2671 $blocks_metadata = static::get_blocks_metadata(); 2672 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); 2673 2674 $filters = ''; 2675 foreach ( $setting_nodes as $metadata ) { 2676 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 2677 if ( empty( $node['color']['duotone'] ) ) { 2678 continue; 2679 } 2680 2681 $duotone_presets = $node['color']['duotone']; 2682 2683 foreach ( $origins as $origin ) { 2684 if ( ! isset( $duotone_presets[ $origin ] ) ) { 2685 continue; 2686 } 2687 foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { 2688 $filters .= wp_get_duotone_filter_svg( $duotone_preset ); 2689 } 2690 } 2691 } 2692 2693 return $filters; 2694 } 2695 2696 /** 2697 * Determines whether a presets should be overridden or not. 2698 * 2699 * @since 5.9.0 2700 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. 2701 * 2702 * @param array $theme_json The theme.json like structure to inspect. 2703 * @param array $path Path to inspect. 2704 * @param bool|array $override Data to compute whether to override the preset. 2705 * @return boolean 2706 */ 2707 protected static function should_override_preset( $theme_json, $path, $override ) { 2708 _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); 2709 2710 if ( is_bool( $override ) ) { 2711 return $override; 2712 } 2713 2714 /* 2715 * The relationship between whether to override the defaults 2716 * and whether the defaults are enabled is inverse: 2717 * 2718 * - If defaults are enabled => theme presets should not be overridden 2719 * - If defaults are disabled => theme presets should be overridden 2720 * 2721 * For example, a theme sets defaultPalette to false, 2722 * making the default palette hidden from the user. 2723 * In that case, we want all the theme presets to be present, 2724 * so they should override the defaults. 2725 */ 2726 if ( is_array( $override ) ) { 2727 $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); 2728 if ( isset( $value ) ) { 2729 return ! $value; 2730 } 2731 2732 // Search the top-level key if none was found for this node. 2733 $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); 2734 if ( isset( $value ) ) { 2735 return ! $value; 2736 } 2737 2738 return true; 2739 } 2740 } 2741 2742 /** 2743 * Returns the default slugs for all the presets in an associative array 2744 * whose keys are the preset paths and the leafs is the list of slugs. 2745 * 2746 * For example: 2747 * 2748 * array( 2749 * 'color' => array( 2750 * 'palette' => array( 'slug-1', 'slug-2' ), 2751 * 'gradients' => array( 'slug-3', 'slug-4' ), 2752 * ), 2753 * ) 2754 * 2755 * @since 5.9.0 2756 * 2757 * @param array $data A theme.json like structure. 2758 * @param array $node_path The path to inspect. It's 'settings' by default. 2759 * @return array 2760 */ 2761 protected static function get_default_slugs( $data, $node_path ) { 2762 $slugs = array(); 2763 2764 foreach ( static::PRESETS_METADATA as $metadata ) { 2765 $path = $node_path; 2766 foreach ( $metadata['path'] as $leaf ) { 2767 $path[] = $leaf; 2768 } 2769 $path[] = 'default'; 2770 2771 $preset = _wp_array_get( $data, $path, null ); 2772 if ( ! isset( $preset ) ) { 2773 continue; 2774 } 2775 2776 $slugs_for_preset = array(); 2777 foreach ( $preset as $item ) { 2778 if ( isset( $item['slug'] ) ) { 2779 $slugs_for_preset[] = $item['slug']; 2780 } 2781 } 2782 2783 _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); 2784 } 2785 2786 return $slugs; 2787 } 2788 2789 /** 2790 * Gets a `default`'s preset name by a provided slug. 2791 * 2792 * @since 5.9.0 2793 * 2794 * @param string $slug The slug we want to find a match from default presets. 2795 * @param array $base_path The path to inspect. It's 'settings' by default. 2796 * @return string|null 2797 */ 2798 protected function get_name_from_defaults( $slug, $base_path ) { 2799 $path = $base_path; 2800 $path[] = 'default'; 2801 $default_content = _wp_array_get( $this->theme_json, $path, null ); 2802 if ( ! $default_content ) { 2803 return null; 2804 } 2805 foreach ( $default_content as $item ) { 2806 if ( $slug === $item['slug'] ) { 2807 return $item['name']; 2808 } 2809 } 2810 return null; 2811 } 2812 2813 /** 2814 * Removes the preset values whose slug is equal to any of given slugs. 2815 * 2816 * @since 5.9.0 2817 * 2818 * @param array $node The node with the presets to validate. 2819 * @param array $slugs The slugs that should not be overridden. 2820 * @return array The new node. 2821 */ 2822 protected static function filter_slugs( $node, $slugs ) { 2823 if ( empty( $slugs ) ) { 2824 return $node; 2825 } 2826 2827 $new_node = array(); 2828 foreach ( $node as $value ) { 2829 if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { 2830 $new_node[] = $value; 2831 } 2832 } 2833 2834 return $new_node; 2835 } 2836 2837 /** 2838 * Removes insecure data from theme.json. 2839 * 2840 * @since 5.9.0 2841 * 2842 * @param array $theme_json Structure to sanitize. 2843 * @return array Sanitized structure. 2844 */ 2845 public static function remove_insecure_properties( $theme_json ) { 2846 $sanitized = array(); 2847 2848 $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); 2849 2850 $valid_block_names = array_keys( static::get_blocks_metadata() ); 2851 $valid_element_names = array_keys( static::ELEMENTS ); 2852 2853 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); 2854 2855 $blocks_metadata = static::get_blocks_metadata(); 2856 $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); 2857 2858 foreach ( $style_nodes as $metadata ) { 2859 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 2860 if ( empty( $input ) ) { 2861 continue; 2862 } 2863 2864 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. 2865 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { 2866 $output = $input; 2867 } else { 2868 $output = static::remove_insecure_styles( $input ); 2869 } 2870 2871 /* 2872 * Get a reference to element name from path. 2873 * $metadata['path'] = array( 'styles', 'elements', 'link' ); 2874 */ 2875 $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; 2876 2877 /* 2878 * $output is stripped of pseudo selectors. Re-add and process them 2879 * or insecure styles here. 2880 */ 2881 // TODO: Replace array_key_exists() with isset() check once WordPress drops 2882 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. 2883 if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { 2884 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { 2885 if ( isset( $input[ $pseudo_selector ] ) ) { 2886 $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); 2887 } 2888 } 2889 } 2890 2891 if ( ! empty( $output ) ) { 2892 _wp_array_set( $sanitized, $metadata['path'], $output ); 2893 } 2894 } 2895 2896 $setting_nodes = static::get_setting_nodes( $theme_json ); 2897 foreach ( $setting_nodes as $metadata ) { 2898 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 2899 if ( empty( $input ) ) { 2900 continue; 2901 } 2902 2903 $output = static::remove_insecure_settings( $input ); 2904 if ( ! empty( $output ) ) { 2905 _wp_array_set( $sanitized, $metadata['path'], $output ); 2906 } 2907 } 2908 2909 if ( empty( $sanitized['styles'] ) ) { 2910 unset( $theme_json['styles'] ); 2911 } else { 2912 $theme_json['styles'] = $sanitized['styles']; 2913 } 2914 2915 if ( empty( $sanitized['settings'] ) ) { 2916 unset( $theme_json['settings'] ); 2917 } else { 2918 $theme_json['settings'] = $sanitized['settings']; 2919 } 2920 2921 return $theme_json; 2922 } 2923 2924 /** 2925 * Processes a setting node and returns the same node 2926 * without the insecure settings. 2927 * 2928 * @since 5.9.0 2929 * 2930 * @param array $input Node to process. 2931 * @return array 2932 */ 2933 protected static function remove_insecure_settings( $input ) { 2934 $output = array(); 2935 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 2936 foreach ( static::VALID_ORIGINS as $origin ) { 2937 $path_with_origin = $preset_metadata['path']; 2938 $path_with_origin[] = $origin; 2939 $presets = _wp_array_get( $input, $path_with_origin, null ); 2940 if ( null === $presets ) { 2941 continue; 2942 } 2943 2944 $escaped_preset = array(); 2945 foreach ( $presets as $preset ) { 2946 if ( 2947 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && 2948 sanitize_html_class( $preset['slug'] ) === $preset['slug'] 2949 ) { 2950 $value = null; 2951 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { 2952 $value = $preset[ $preset_metadata['value_key'] ]; 2953 } elseif ( 2954 isset( $preset_metadata['value_func'] ) && 2955 is_callable( $preset_metadata['value_func'] ) 2956 ) { 2957 $value = call_user_func( $preset_metadata['value_func'], $preset ); 2958 } 2959 2960 $preset_is_valid = true; 2961 foreach ( $preset_metadata['properties'] as $property ) { 2962 if ( ! static::is_safe_css_declaration( $property, $value ) ) { 2963 $preset_is_valid = false; 2964 break; 2965 } 2966 } 2967 2968 if ( $preset_is_valid ) { 2969 $escaped_preset[] = $preset; 2970 } 2971 } 2972 } 2973 2974 if ( ! empty( $escaped_preset ) ) { 2975 _wp_array_set( $output, $path_with_origin, $escaped_preset ); 2976 } 2977 } 2978 } 2979 2980 // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed. 2981 static::remove_indirect_properties( $input, $output ); 2982 2983 return $output; 2984 } 2985 2986 /** 2987 * Processes a style node and returns the same node 2988 * without the insecure styles. 2989 * 2990 * @since 5.9.0 2991 * 2992 * @param array $input Node to process. 2993 * @return array 2994 */ 2995 protected static function remove_insecure_styles( $input ) { 2996 $output = array(); 2997 $declarations = static::compute_style_properties( $input ); 2998 2999 foreach ( $declarations as $declaration ) { 3000 if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { 3001 $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; 3002 3003 // Check the value isn't an array before adding so as to not 3004 // double up shorthand and longhand styles. 3005 $value = _wp_array_get( $input, $path, array() ); 3006 if ( ! is_array( $value ) ) { 3007 _wp_array_set( $output, $path, $value ); 3008 } 3009 } 3010 } 3011 3012 // Ensure indirect properties not handled by `compute_style_properties` are allowed. 3013 static::remove_indirect_properties( $input, $output ); 3014 3015 return $output; 3016 } 3017 3018 /** 3019 * Checks that a declaration provided by the user is safe. 3020 * 3021 * @since 5.9.0 3022 * 3023 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. 3024 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. 3025 * @return bool 3026 */ 3027 protected static function is_safe_css_declaration( $property_name, $property_value ) { 3028 $style_to_validate = $property_name . ': ' . $property_value; 3029 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); 3030 return ! empty( trim( $filtered ) ); 3031 } 3032 3033 /** 3034 * Removes indirect properties from the given input node and 3035 * sets in the given output node. 3036 * 3037 * @since 6.2.0 3038 * 3039 * @param array $input Node to process. 3040 * @param array $output The processed node. Passed by reference. 3041 */ 3042 private static function remove_indirect_properties( $input, &$output ) { 3043 foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { 3044 foreach ( $paths as $path ) { 3045 $value = _wp_array_get( $input, $path ); 3046 if ( 3047 is_string( $value ) && 3048 static::is_safe_css_declaration( $property, $value ) 3049 ) { 3050 _wp_array_set( $output, $path, $value ); 3051 } 3052 } 3053 } 3054 } 3055 3056 /** 3057 * Returns the raw data. 3058 * 3059 * @since 5.8.0 3060 * 3061 * @return array Raw data. 3062 */ 3063 public function get_raw_data() { 3064 return $this->theme_json; 3065 } 3066 3067 /** 3068 * Transforms the given editor settings according the 3069 * add_theme_support format to the theme.json format. 3070 * 3071 * @since 5.8.0 3072 * 3073 * @param array $settings Existing editor settings. 3074 * @return array Config that adheres to the theme.json schema. 3075 */ 3076 public static function get_from_editor_settings( $settings ) { 3077 $theme_settings = array( 3078 'version' => static::LATEST_SCHEMA, 3079 'settings' => array(), 3080 ); 3081 3082 // Deprecated theme supports. 3083 if ( isset( $settings['disableCustomColors'] ) ) { 3084 if ( ! isset( $theme_settings['settings']['color'] ) ) { 3085 $theme_settings['settings']['color'] = array(); 3086 } 3087 $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; 3088 } 3089 3090 if ( isset( $settings['disableCustomGradients'] ) ) { 3091 if ( ! isset( $theme_settings['settings']['color'] ) ) { 3092 $theme_settings['settings']['color'] = array(); 3093 } 3094 $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; 3095 } 3096 3097 if ( isset( $settings['disableCustomFontSizes'] ) ) { 3098 if ( ! isset( $theme_settings['settings']['typography'] ) ) { 3099 $theme_settings['settings']['typography'] = array(); 3100 } 3101 $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; 3102 } 3103 3104 if ( isset( $settings['enableCustomLineHeight'] ) ) { 3105 if ( ! isset( $theme_settings['settings']['typography'] ) ) { 3106 $theme_settings['settings']['typography'] = array(); 3107 } 3108 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; 3109 } 3110 3111 if ( isset( $settings['enableCustomUnits'] ) ) { 3112 if ( ! isset( $theme_settings['settings']['spacing'] ) ) { 3113 $theme_settings['settings']['spacing'] = array(); 3114 } 3115 $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? 3116 array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : 3117 $settings['enableCustomUnits']; 3118 } 3119 3120 if ( isset( $settings['colors'] ) ) { 3121 if ( ! isset( $theme_settings['settings']['color'] ) ) { 3122 $theme_settings['settings']['color'] = array(); 3123 } 3124 $theme_settings['settings']['color']['palette'] = $settings['colors']; 3125 } 3126 3127 if ( isset( $settings['gradients'] ) ) { 3128 if ( ! isset( $theme_settings['settings']['color'] ) ) { 3129 $theme_settings['settings']['color'] = array(); 3130 } 3131 $theme_settings['settings']['color']['gradients'] = $settings['gradients']; 3132 } 3133 3134 if ( isset( $settings['fontSizes'] ) ) { 3135 $font_sizes = $settings['fontSizes']; 3136 // Back-compatibility for presets without units. 3137 foreach ( $font_sizes as $key => $font_size ) { 3138 if ( is_numeric( $font_size['size'] ) ) { 3139 $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; 3140 } 3141 } 3142 if ( ! isset( $theme_settings['settings']['typography'] ) ) { 3143 $theme_settings['settings']['typography'] = array(); 3144 } 3145 $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; 3146 } 3147 3148 if ( isset( $settings['enableCustomSpacing'] ) ) { 3149 if ( ! isset( $theme_settings['settings']['spacing'] ) ) { 3150 $theme_settings['settings']['spacing'] = array(); 3151 } 3152 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; 3153 } 3154 3155 return $theme_settings; 3156 } 3157 3158 /** 3159 * Returns the current theme's wanted patterns(slugs) to be 3160 * registered from Pattern Directory. 3161 * 3162 * @since 6.0.0 3163 * 3164 * @return string[] 3165 */ 3166 public function get_patterns() { 3167 if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { 3168 return $this->theme_json['patterns']; 3169 } 3170 return array(); 3171 } 3172 3173 /** 3174 * Returns a valid theme.json as provided by a theme. 3175 * 3176 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. 3177 * This also uses appearanceTools instead of their opt-ins if all of them are true. 3178 * 3179 * @since 6.0.0 3180 * 3181 * @return array 3182 */ 3183 public function get_data() { 3184 $output = $this->theme_json; 3185 $nodes = static::get_setting_nodes( $output ); 3186 3187 /** 3188 * Flatten the theme & custom origins into a single one. 3189 * 3190 * For example, the following: 3191 * 3192 * { 3193 * "settings": { 3194 * "color": { 3195 * "palette": { 3196 * "theme": [ {} ], 3197 * "custom": [ {} ] 3198 * } 3199 * } 3200 * } 3201 * } 3202 * 3203 * will be converted to: 3204 * 3205 * { 3206 * "settings": { 3207 * "color": { 3208 * "palette": [ {} ] 3209 * } 3210 * } 3211 * } 3212 */ 3213 foreach ( $nodes as $node ) { 3214 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 3215 $path = $node['path']; 3216 foreach ( $preset_metadata['path'] as $preset_metadata_path ) { 3217 $path[] = $preset_metadata_path; 3218 } 3219 $preset = _wp_array_get( $output, $path, null ); 3220 if ( null === $preset ) { 3221 continue; 3222 } 3223 3224 $items = array(); 3225 if ( isset( $preset['theme'] ) ) { 3226 foreach ( $preset['theme'] as $item ) { 3227 $slug = $item['slug']; 3228 unset( $item['slug'] ); 3229 $items[ $slug ] = $item; 3230 } 3231 } 3232 if ( isset( $preset['custom'] ) ) { 3233 foreach ( $preset['custom'] as $item ) { 3234 $slug = $item['slug']; 3235 unset( $item['slug'] ); 3236 $items[ $slug ] = $item; 3237 } 3238 } 3239 $flattened_preset = array(); 3240 foreach ( $items as $slug => $value ) { 3241 $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); 3242 } 3243 _wp_array_set( $output, $path, $flattened_preset ); 3244 } 3245 } 3246 3247 // If all of the static::APPEARANCE_TOOLS_OPT_INS are true, 3248 // this code unsets them and sets 'appearanceTools' instead. 3249 foreach ( $nodes as $node ) { 3250 $all_opt_ins_are_set = true; 3251 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { 3252 $full_path = $node['path']; 3253 foreach ( $opt_in_path as $opt_in_path_item ) { 3254 $full_path[] = $opt_in_path_item; 3255 } 3256 // Use "unset prop" as a marker instead of "null" because 3257 // "null" can be a valid value for some props (e.g. blockGap). 3258 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); 3259 if ( 'unset prop' === $opt_in_value ) { 3260 $all_opt_ins_are_set = false; 3261 break; 3262 } 3263 } 3264 3265 if ( $all_opt_ins_are_set ) { 3266 $node_path_with_appearance_tools = $node['path']; 3267 $node_path_with_appearance_tools[] = 'appearanceTools'; 3268 _wp_array_set( $output, $node_path_with_appearance_tools, true ); 3269 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { 3270 $full_path = $node['path']; 3271 foreach ( $opt_in_path as $opt_in_path_item ) { 3272 $full_path[] = $opt_in_path_item; 3273 } 3274 // Use "unset prop" as a marker instead of "null" because 3275 // "null" can be a valid value for some props (e.g. blockGap). 3276 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); 3277 if ( true !== $opt_in_value ) { 3278 continue; 3279 } 3280 3281 // The following could be improved to be path independent. 3282 // At the moment it relies on a couple of assumptions: 3283 // 3284 // - all opt-ins having a path of size 2. 3285 // - there's two sources of settings: the top-level and the block-level. 3286 if ( 3287 ( 1 === count( $node['path'] ) ) && 3288 ( 'settings' === $node['path'][0] ) 3289 ) { 3290 // Top-level settings. 3291 unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); 3292 if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { 3293 unset( $output['settings'][ $opt_in_path[0] ] ); 3294 } 3295 } elseif ( 3296 ( 3 === count( $node['path'] ) ) && 3297 ( 'settings' === $node['path'][0] ) && 3298 ( 'blocks' === $node['path'][1] ) 3299 ) { 3300 // Block-level settings. 3301 $block_name = $node['path'][2]; 3302 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); 3303 if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { 3304 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); 3305 } 3306 } 3307 } 3308 } 3309 } 3310 3311 wp_recursive_ksort( $output ); 3312 3313 return $output; 3314 } 3315 3316 /** 3317 * Sets the spacingSizes array based on the spacingScale values from theme.json. 3318 * 3319 * @since 6.1.0 3320 * 3321 * @return null|void 3322 */ 3323 public function set_spacing_sizes() { 3324 $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() ); 3325 3326 if ( ! isset( $spacing_scale['steps'] ) 3327 || ! is_numeric( $spacing_scale['steps'] ) 3328 || ! isset( $spacing_scale['mediumStep'] ) 3329 || ! isset( $spacing_scale['unit'] ) 3330 || ! isset( $spacing_scale['operator'] ) 3331 || ! isset( $spacing_scale['increment'] ) 3332 || ! isset( $spacing_scale['steps'] ) 3333 || ! is_numeric( $spacing_scale['increment'] ) 3334 || ! is_numeric( $spacing_scale['mediumStep'] ) 3335 || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { 3336 if ( ! empty( $spacing_scale ) ) { 3337 trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ), E_USER_NOTICE ); 3338 } 3339 return null; 3340 } 3341 3342 // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0. 3343 if ( 0 === $spacing_scale['steps'] ) { 3344 return null; 3345 } 3346 3347 $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); 3348 $current_step = $spacing_scale['mediumStep']; 3349 $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); 3350 $x_small_count = null; 3351 $below_sizes = array(); 3352 $slug = 40; 3353 $remainder = 0; 3354 3355 for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) { 3356 if ( '+' === $spacing_scale['operator'] ) { 3357 $current_step -= $spacing_scale['increment']; 3358 } elseif ( $spacing_scale['increment'] > 1 ) { 3359 $current_step /= $spacing_scale['increment']; 3360 } else { 3361 $current_step *= $spacing_scale['increment']; 3362 } 3363 3364 if ( $current_step <= 0 ) { 3365 $remainder = $below_midpoint_count; 3366 break; 3367 } 3368 3369 $below_sizes[] = array( 3370 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ 3371 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ), 3372 'slug' => (string) $slug, 3373 'size' => round( $current_step, 2 ) . $unit, 3374 ); 3375 3376 if ( $below_midpoint_count === $steps_mid_point - 2 ) { 3377 $x_small_count = 2; 3378 } 3379 3380 if ( $below_midpoint_count < $steps_mid_point - 2 ) { 3381 $x_small_count++; 3382 } 3383 3384 $slug -= 10; 3385 } 3386 3387 $below_sizes = array_reverse( $below_sizes ); 3388 3389 $below_sizes[] = array( 3390 'name' => __( 'Medium' ), 3391 'slug' => '50', 3392 'size' => $spacing_scale['mediumStep'] . $unit, 3393 ); 3394 3395 $current_step = $spacing_scale['mediumStep']; 3396 $x_large_count = null; 3397 $above_sizes = array(); 3398 $slug = 60; 3399 $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; 3400 3401 for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) { 3402 $current_step = '+' === $spacing_scale['operator'] 3403 ? $current_step + $spacing_scale['increment'] 3404 : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); 3405 3406 $above_sizes[] = array( 3407 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ 3408 'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ), 3409 'slug' => (string) $slug, 3410 'size' => round( $current_step, 2 ) . $unit, 3411 ); 3412 3413 if ( 1 === $above_midpoint_count ) { 3414 $x_large_count = 2; 3415 } 3416 3417 if ( $above_midpoint_count > 1 ) { 3418 $x_large_count++; 3419 } 3420 3421 $slug += 10; 3422 } 3423 3424 $spacing_sizes = $below_sizes; 3425 foreach ( $above_sizes as $above_sizes_item ) { 3426 $spacing_sizes[] = $above_sizes_item; 3427 } 3428 3429 // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. 3430 if ( $spacing_scale['steps'] <= 7 ) { 3431 for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { 3432 $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); 3433 } 3434 } 3435 3436 _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); 3437 } 3438 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Sun Jun 4 08:20:02 2023 | Cross-referenced by PHPXref |