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