| [ 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 * @since 6.6.0 Added the `dimensions.aspectRatios` and `dimensions.defaultAspectRatios` presets. 127 * Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' 128 * and spacing size presets to use `spacing.defaultSpacingSizes`. 129 * @since 6.9.0 Added `border.radiusSizes`. 130 * @var array 131 */ 132 const PRESETS_METADATA = array( 133 array( 134 'path' => array( 'dimensions', 'aspectRatios' ), 135 'prevent_override' => array( 'dimensions', 'defaultAspectRatios' ), 136 'use_default_names' => false, 137 'value_key' => 'ratio', 138 'css_vars' => '--wp--preset--aspect-ratio--$slug', 139 'classes' => array(), 140 'properties' => array( 'aspect-ratio' ), 141 ), 142 array( 143 'path' => array( 'color', 'palette' ), 144 'prevent_override' => array( 'color', 'defaultPalette' ), 145 'use_default_names' => false, 146 'value_key' => 'color', 147 'css_vars' => '--wp--preset--color--$slug', 148 'classes' => array( 149 '.has-$slug-color' => 'color', 150 '.has-$slug-background-color' => 'background-color', 151 '.has-$slug-border-color' => 'border-color', 152 ), 153 'properties' => array( 'color', 'background-color', 'border-color' ), 154 ), 155 array( 156 'path' => array( 'color', 'gradients' ), 157 'prevent_override' => array( 'color', 'defaultGradients' ), 158 'use_default_names' => false, 159 'value_key' => 'gradient', 160 'css_vars' => '--wp--preset--gradient--$slug', 161 'classes' => array( '.has-$slug-gradient-background' => 'background' ), 162 'properties' => array( 'background' ), 163 ), 164 array( 165 'path' => array( 'color', 'duotone' ), 166 'prevent_override' => array( 'color', 'defaultDuotone' ), 167 'use_default_names' => false, 168 'value_func' => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone.php. 169 'css_vars' => null, 170 'classes' => array(), 171 'properties' => array( 'filter' ), 172 ), 173 array( 174 'path' => array( 'typography', 'fontSizes' ), 175 'prevent_override' => array( 'typography', 'defaultFontSizes' ), 176 'use_default_names' => true, 177 'value_func' => 'wp_get_typography_font_size_value', 178 'css_vars' => '--wp--preset--font-size--$slug', 179 'classes' => array( '.has-$slug-font-size' => 'font-size' ), 180 'properties' => array( 'font-size' ), 181 ), 182 array( 183 'path' => array( 'typography', 'fontFamilies' ), 184 'prevent_override' => false, 185 'use_default_names' => false, 186 'value_key' => 'fontFamily', 187 'css_vars' => '--wp--preset--font-family--$slug', 188 'classes' => array( '.has-$slug-font-family' => 'font-family' ), 189 'properties' => array( 'font-family' ), 190 ), 191 array( 192 'path' => array( 'spacing', 'spacingSizes' ), 193 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ), 194 'use_default_names' => true, 195 'value_key' => 'size', 196 'css_vars' => '--wp--preset--spacing--$slug', 197 'classes' => array(), 198 'properties' => array( 'padding', 'margin' ), 199 ), 200 array( 201 'path' => array( 'shadow', 'presets' ), 202 'prevent_override' => array( 'shadow', 'defaultPresets' ), 203 'use_default_names' => false, 204 'value_key' => 'shadow', 205 'css_vars' => '--wp--preset--shadow--$slug', 206 'classes' => array(), 207 'properties' => array( 'box-shadow' ), 208 ), 209 array( 210 'path' => array( 'border', 'radiusSizes' ), 211 'prevent_override' => false, 212 'use_default_names' => false, 213 'value_key' => 'size', 214 'css_vars' => '--wp--preset--border-radius--$slug', 215 'classes' => array(), 216 'properties' => array( 'border-radius' ), 217 ), 218 array( 219 'path' => array( 'dimensions', 'dimensionSizes' ), 220 'prevent_override' => false, 221 'use_default_names' => false, 222 'value_key' => 'size', 223 'css_vars' => '--wp--preset--dimension--$slug', 224 'classes' => array(), 225 'properties' => array( 'width', 'height', 'min-height' ), 226 ), 227 ); 228 229 /** 230 * Metadata for style properties. 231 * 232 * Each element is a direct mapping from the CSS property name to the 233 * path to the value in theme.json & block attributes. 234 * 235 * @since 5.8.0 236 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`, 237 * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`, 238 * `text-decoration`, `text-transform`, and `filter` properties, 239 * simplified the metadata structure. 240 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`, 241 * `--wp--style--root--padding-*`, and `box-shadow` properties, 242 * removed the `--wp--style--block-gap` property. 243 * @since 6.2.0 Added `outline-*`, and `min-height` properties. 244 * @since 6.3.0 Added `column-count` property. 245 * @since 6.4.0 Added `writing-mode` property. 246 * @since 6.5.0 Added `aspect-ratio` property. 247 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties. 248 * @since 6.7.0 Added `background-attachment` property. 249 * @since 7.0.0 Added `dimensions.width` and `dimensions.height`. 250 * Added `text-indent` property. 251 * @var array 252 */ 253 const PROPERTIES_METADATA = array( 254 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ), 255 'background' => array( 'color', 'gradient' ), 256 'background-color' => array( 'color', 'background' ), 257 'background-image' => array( 'background', 'backgroundImage' ), 258 'background-position' => array( 'background', 'backgroundPosition' ), 259 'background-repeat' => array( 'background', 'backgroundRepeat' ), 260 'background-size' => array( 'background', 'backgroundSize' ), 261 'background-attachment' => array( 'background', 'backgroundAttachment' ), 262 'border-radius' => array( 'border', 'radius' ), 263 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 264 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), 265 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), 266 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), 267 'border-color' => array( 'border', 'color' ), 268 'border-width' => array( 'border', 'width' ), 269 'border-style' => array( 'border', 'style' ), 270 'border-top-color' => array( 'border', 'top', 'color' ), 271 'border-top-width' => array( 'border', 'top', 'width' ), 272 'border-top-style' => array( 'border', 'top', 'style' ), 273 'border-right-color' => array( 'border', 'right', 'color' ), 274 'border-right-width' => array( 'border', 'right', 'width' ), 275 'border-right-style' => array( 'border', 'right', 'style' ), 276 'border-bottom-color' => array( 'border', 'bottom', 'color' ), 277 'border-bottom-width' => array( 'border', 'bottom', 'width' ), 278 'border-bottom-style' => array( 'border', 'bottom', 'style' ), 279 'border-left-color' => array( 'border', 'left', 'color' ), 280 'border-left-width' => array( 'border', 'left', 'width' ), 281 'border-left-style' => array( 'border', 'left', 'style' ), 282 'color' => array( 'color', 'text' ), 283 'text-align' => array( 'typography', 'textAlign' ), 284 'column-count' => array( 'typography', 'textColumns' ), 285 'font-family' => array( 'typography', 'fontFamily' ), 286 'font-size' => array( 'typography', 'fontSize' ), 287 'font-style' => array( 'typography', 'fontStyle' ), 288 'font-weight' => array( 'typography', 'fontWeight' ), 289 'letter-spacing' => array( 'typography', 'letterSpacing' ), 290 'line-height' => array( 'typography', 'lineHeight' ), 291 'margin' => array( 'spacing', 'margin' ), 292 'margin-top' => array( 'spacing', 'margin', 'top' ), 293 'margin-right' => array( 'spacing', 'margin', 'right' ), 294 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), 295 'margin-left' => array( 'spacing', 'margin', 'left' ), 296 'min-height' => array( 'dimensions', 'minHeight' ), 297 'outline-color' => array( 'outline', 'color' ), 298 'outline-offset' => array( 'outline', 'offset' ), 299 'outline-style' => array( 'outline', 'style' ), 300 'outline-width' => array( 'outline', 'width' ), 301 'padding' => array( 'spacing', 'padding' ), 302 'padding-top' => array( 'spacing', 'padding', 'top' ), 303 'padding-right' => array( 'spacing', 'padding', 'right' ), 304 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), 305 'padding-left' => array( 'spacing', 'padding', 'left' ), 306 '--wp--style--root--padding' => array( 'spacing', 'padding' ), 307 '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ), 308 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ), 309 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ), 310 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ), 311 'text-decoration' => array( 'typography', 'textDecoration' ), 312 'text-transform' => array( 'typography', 'textTransform' ), 313 'text-indent' => array( 'typography', 'textIndent' ), 314 'filter' => array( 'filter', 'duotone' ), 315 'box-shadow' => array( 'shadow' ), 316 'height' => array( 'dimensions', 'height' ), 317 'width' => array( 'dimensions', 'width' ), 318 'writing-mode' => array( 'typography', 'writingMode' ), 319 ); 320 321 /** 322 * Indirect metadata for style properties that are not directly output. 323 * 324 * Each element maps from a CSS property name to an array of 325 * paths to the value in theme.json & block attributes. 326 * 327 * Indirect properties are not output directly by `compute_style_properties`, 328 * but are used elsewhere in the processing of global styles. The indirect 329 * property is used to validate whether a style value is allowed. 330 * 331 * @since 6.2.0 332 * @since 6.6.0 Added background-image properties. 333 * @var array 334 */ 335 const INDIRECT_PROPERTIES_METADATA = array( 336 'gap' => array( 337 array( 'spacing', 'blockGap' ), 338 ), 339 'column-gap' => array( 340 array( 'spacing', 'blockGap', 'left' ), 341 ), 342 'row-gap' => array( 343 array( 'spacing', 'blockGap', 'top' ), 344 ), 345 'max-width' => array( 346 array( 'layout', 'contentSize' ), 347 array( 'layout', 'wideSize' ), 348 ), 349 'background-image' => array( 350 array( 'background', 'backgroundImage', 'url' ), 351 ), 352 ); 353 354 /** 355 * Protected style properties. 356 * 357 * These style properties are only rendered if a setting enables it 358 * via a value other than `null`. 359 * 360 * Each element maps the style property to the corresponding theme.json 361 * setting key. 362 * 363 * @since 5.9.0 364 * @var array 365 */ 366 const PROTECTED_PROPERTIES = array( 367 'spacing.blockGap' => array( 'spacing', 'blockGap' ), 368 ); 369 370 /** 371 * The top-level keys a theme.json can have. 372 * 373 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`. 374 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, 375 * added the `customTemplates` and `templateParts` values. 376 * @since 6.3.0 Added the `description` value. 377 * @since 6.6.0 Added `blockTypes` to support block style variation theme.json partials. 378 * @var string[] 379 */ 380 const VALID_TOP_LEVEL_KEYS = array( 381 'blockTypes', 382 'customTemplates', 383 'description', 384 'patterns', 385 'settings', 386 'slug', 387 'styles', 388 'templateParts', 389 'title', 390 'version', 391 ); 392 393 /** 394 * The valid properties under the settings key. 395 * 396 * @since 5.8.0 As `ALLOWED_SETTINGS`. 397 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, 398 * added new properties for `border`, `color`, `spacing`, 399 * and `typography`, and renamed others according to the new schema. 400 * @since 6.0.0 Added `color.defaultDuotone`. 401 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. 402 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets', 403 * `position.fixed` and `position.sticky`. 404 * @since 6.3.0 Added support for `typography.textColumns`, removed `layout.definitions`. 405 * @since 6.4.0 Added support for `layout.allowEditing`, `background.backgroundImage`, 406 * `typography.writingMode`, `lightbox.enabled` and `lightbox.allowEditing`. 407 * @since 6.5.0 Added support for `layout.allowCustomContentAndWideSize`, 408 * `background.backgroundSize` and `dimensions.aspectRatio`. 409 * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios', 410 * 'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'. 411 * @since 6.9.0 Added support for `border.radiusSizes`. 412 * @since 7.0.0 Added type markers to the schema for boolean values. 413 * Added support for `dimensions.width` and `dimensions.height`. 414 * Added support for `typography.textIndent`. 415 * @var array 416 */ 417 const VALID_SETTINGS = array( 418 'appearanceTools' => null, 419 'useRootPaddingAwareAlignments' => null, 420 'background' => array( 421 'backgroundImage' => null, 422 'backgroundSize' => null, 423 ), 424 'border' => array( 425 'color' => null, 426 'radius' => null, 427 'radiusSizes' => null, 428 'style' => null, 429 'width' => null, 430 ), 431 'color' => array( 432 'background' => null, 433 'custom' => null, 434 'customDuotone' => null, 435 'customGradient' => null, 436 'defaultDuotone' => null, 437 'defaultGradients' => null, 438 'defaultPalette' => null, 439 'duotone' => null, 440 'gradients' => null, 441 'link' => null, 442 'heading' => null, 443 'button' => null, 444 'caption' => null, 445 'palette' => null, 446 'text' => null, 447 ), 448 'custom' => null, 449 'dimensions' => array( 450 'aspectRatio' => null, 451 'aspectRatios' => null, 452 'defaultAspectRatios' => null, 453 'dimensionSizes' => null, 454 'height' => null, 455 'minHeight' => null, 456 'width' => null, 457 ), 458 'layout' => array( 459 'contentSize' => null, 460 'wideSize' => null, 461 'allowEditing' => null, 462 'allowCustomContentAndWideSize' => null, 463 ), 464 'lightbox' => array( 465 'enabled' => true, 466 'allowEditing' => true, 467 ), 468 'position' => array( 469 'fixed' => null, 470 'sticky' => null, 471 ), 472 'spacing' => array( 473 'customSpacingSize' => null, 474 'defaultSpacingSizes' => null, 475 'spacingSizes' => null, 476 'spacingScale' => null, 477 'blockGap' => null, 478 'margin' => null, 479 'padding' => null, 480 'units' => null, 481 ), 482 'shadow' => array( 483 'presets' => null, 484 'defaultPresets' => null, 485 ), 486 'typography' => array( 487 'fluid' => null, 488 'customFontSize' => null, 489 'defaultFontSizes' => null, 490 'dropCap' => null, 491 'fontFamilies' => null, 492 'fontSizes' => null, 493 'fontStyle' => null, 494 'fontWeight' => null, 495 'letterSpacing' => null, 496 'lineHeight' => null, 497 'textAlign' => null, 498 'textColumns' => null, 499 'textDecoration' => null, 500 'textIndent' => null, 501 'textTransform' => null, 502 'writingMode' => null, 503 ), 504 ); 505 506 /** 507 * The valid properties for fontFamilies under settings key. 508 * 509 * @since 6.5.0 510 * @var array 511 */ 512 const FONT_FAMILY_SCHEMA = array( 513 array( 514 'fontFamily' => null, 515 'name' => null, 516 'slug' => null, 517 'fontFace' => array( 518 array( 519 'ascentOverride' => null, 520 'descentOverride' => null, 521 'fontDisplay' => null, 522 'fontFamily' => null, 523 'fontFeatureSettings' => null, 524 'fontStyle' => null, 525 'fontStretch' => null, 526 'fontVariationSettings' => null, 527 'fontWeight' => null, 528 'lineGapOverride' => null, 529 'sizeAdjust' => null, 530 'src' => null, 531 'unicodeRange' => null, 532 ), 533 ), 534 ), 535 ); 536 537 /** 538 * The valid properties under the styles key. 539 * 540 * @since 5.8.0 As `ALLOWED_STYLES`. 541 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, 542 * added new properties for `border`, `filter`, `spacing`, 543 * and `typography`. 544 * @since 6.1.0 Added new side properties for `border`, 545 * added new property `shadow`, 546 * updated `blockGap` to be allowed at any level. 547 * @since 6.2.0 Added `outline`, and `minHeight` properties. 548 * @since 6.3.0 Added support for `typography.textColumns`. 549 * @since 6.5.0 Added support for `dimensions.aspectRatio`. 550 * @since 6.6.0 Added `background` sub properties to top-level only. 551 * @since 7.0.0 Added support for `dimensions.width` and `dimensions.height`. 552 * @var array 553 */ 554 const VALID_STYLES = array( 555 'background' => array( 556 'backgroundImage' => null, 557 'backgroundPosition' => null, 558 'backgroundRepeat' => null, 559 'backgroundSize' => null, 560 'backgroundAttachment' => null, 561 ), 562 'border' => array( 563 'color' => null, 564 'radius' => null, 565 'style' => null, 566 'width' => null, 567 'top' => null, 568 'right' => null, 569 'bottom' => null, 570 'left' => null, 571 ), 572 'color' => array( 573 'background' => null, 574 'gradient' => null, 575 'text' => null, 576 ), 577 'dimensions' => array( 578 'aspectRatio' => null, 579 'height' => null, 580 'minHeight' => null, 581 'width' => null, 582 ), 583 'filter' => array( 584 'duotone' => null, 585 ), 586 'outline' => array( 587 'color' => null, 588 'offset' => null, 589 'style' => null, 590 'width' => null, 591 ), 592 'shadow' => null, 593 'spacing' => array( 594 'margin' => null, 595 'padding' => null, 596 'blockGap' => null, 597 ), 598 'typography' => array( 599 'fontFamily' => null, 600 'fontSize' => null, 601 'fontStyle' => null, 602 'fontWeight' => null, 603 'letterSpacing' => null, 604 'lineHeight' => null, 605 'textAlign' => null, 606 'textColumns' => null, 607 'textDecoration' => null, 608 'textIndent' => null, 609 'textTransform' => null, 610 'writingMode' => null, 611 ), 612 'css' => null, 613 ); 614 615 /** 616 * Defines which pseudo selectors are enabled for which elements. 617 * 618 * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active. 619 * This is to ensure the user action (hover, focus and active) styles have a higher 620 * specificity than the visited styles, which in turn have a higher specificity than 621 * the unvisited styles. 622 * 623 * See https://core.trac.wordpress.org/ticket/56928. 624 * Note: this will affect both top-level and block-level elements. 625 * 626 * @since 6.1.0 627 * @since 6.2.0 Added support for ':link' and ':any-link'. 628 * @since 6.8.0 Added support for ':focus-visible'. 629 * @since 6.9.0 Added `textInput` and `select` elements. 630 * @var array 631 */ 632 const VALID_ELEMENT_PSEUDO_SELECTORS = array( 633 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), 634 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), 635 ); 636 637 /** 638 * The valid pseudo-selectors that can be used for blocks. 639 * 640 * @since 7.0 641 * @var array 642 */ 643 const VALID_BLOCK_PSEUDO_SELECTORS = array( 644 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ), 645 ); 646 647 /** 648 * Responsive breakpoint state keys and their corresponding CSS media queries. 649 * These are available for all blocks and wrap their styles in the given media query. 650 * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx. 651 * 652 * @since 7.1.0 653 * @var array 654 */ 655 const RESPONSIVE_BREAKPOINTS = array( 656 '@mobile' => '@media (width <= 480px)', 657 '@tablet' => '@media (480px < width <= 782px)', 658 ); 659 660 /** 661 * The valid elements that can be found under styles. 662 * 663 * @since 5.8.0 664 * @since 6.1.0 Added `heading`, `button`, and `caption` elements. 665 * @var string[] 666 */ 667 const ELEMENTS = array( 668 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity. 669 'heading' => 'h1, h2, h3, h4, h5, h6', 670 'h1' => 'h1', 671 'h2' => 'h2', 672 'h3' => 'h3', 673 'h4' => 'h4', 674 'h5' => 'h5', 675 'h6' => 'h6', 676 // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. 677 'button' => '.wp-element-button, .wp-block-button__link', 678 // The block classes are necessary to target older content that won't use the new class names. 679 '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', 680 'cite' => 'cite', 681 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])', 682 'select' => 'select', 683 ); 684 685 const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( 686 'button' => 'wp-element-button', 687 'caption' => 'wp-element-caption', 688 ); 689 690 /** 691 * List of block support features that can have their related styles 692 * generated under their own feature level selector rather than the block's. 693 * 694 * @since 6.1.0 695 * @since 7.0.0 Added support for `dimensions`. 696 * @var string[] 697 */ 698 const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( 699 '__experimentalBorder' => 'border', 700 'color' => 'color', 701 'dimensions' => 'dimensions', 702 'spacing' => 'spacing', 703 'typography' => 'typography', 704 ); 705 706 /** 707 * Return the input schema at the root and per origin. 708 * 709 * @since 6.5.0 710 * 711 * @param array $schema The base schema. 712 * @return array The schema at the root and per origin. 713 * 714 * Example: 715 * schema_in_root_and_per_origin( 716 * array( 717 * 'fontFamily' => null, 718 * 'slug' => null, 719 * ) 720 * ) 721 * 722 * Returns: 723 * array( 724 * 'fontFamily' => null, 725 * 'slug' => null, 726 * 'default' => array( 727 * 'fontFamily' => null, 728 * 'slug' => null, 729 * ), 730 * 'blocks' => array( 731 * 'fontFamily' => null, 732 * 'slug' => null, 733 * ), 734 * 'theme' => array( 735 * 'fontFamily' => null, 736 * 'slug' => null, 737 * ), 738 * 'custom' => array( 739 * 'fontFamily' => null, 740 * 'slug' => null, 741 * ), 742 * ) 743 */ 744 protected static function schema_in_root_and_per_origin( $schema ) { 745 $schema_in_root_and_per_origin = $schema; 746 foreach ( static::VALID_ORIGINS as $origin ) { 747 $schema_in_root_and_per_origin[ $origin ] = $schema; 748 } 749 return $schema_in_root_and_per_origin; 750 } 751 752 753 /** 754 * Processes pseudo-selectors for any node (block or variation). 755 * 756 * @param array $node The node data (block or variation). 757 * @param string $base_selector The base selector. 758 * @param array $settings The theme settings. 759 * @param string $block_name The block name. 760 * @return array Array of pseudo-selector declarations. 761 */ 762 private static function process_pseudo_selectors( $node, $base_selector, $settings, $block_name ) { 763 $pseudo_declarations = array(); 764 765 if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { 766 return $pseudo_declarations; 767 } 768 769 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { 770 if ( isset( $node[ $pseudo_selector ] ) ) { 771 $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector ); 772 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null ); 773 $pseudo_declarations[ $combined_selector ] = $declarations; 774 } 775 } 776 777 return $pseudo_declarations; 778 } 779 780 781 /** 782 * Returns a class name by an element name. 783 * 784 * @since 6.1.0 785 * 786 * @param string $element The name of the element. 787 * @return string The name of the class. 788 */ 789 public static function get_element_class_name( $element ) { 790 $class_name = ''; 791 792 if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) { 793 $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ]; 794 } 795 796 return $class_name; 797 } 798 799 /** 800 * Options that settings.appearanceTools enables. 801 * 802 * @since 6.0.0 803 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`. 804 * @since 6.4.0 Added `background.backgroundImage`. 805 * @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`. 806 * @since 7.0.0 Added `dimensions.width` and `dimensions.height`. 807 * @var array 808 */ 809 const APPEARANCE_TOOLS_OPT_INS = array( 810 array( 'background', 'backgroundImage' ), 811 array( 'background', 'backgroundSize' ), 812 array( 'border', 'color' ), 813 array( 'border', 'radius' ), 814 array( 'border', 'style' ), 815 array( 'border', 'width' ), 816 array( 'color', 'link' ), 817 array( 'color', 'heading' ), 818 array( 'color', 'button' ), 819 array( 'color', 'caption' ), 820 array( 'dimensions', 'aspectRatio' ), 821 array( 'dimensions', 'height' ), 822 array( 'dimensions', 'minHeight' ), 823 array( 'dimensions', 'width' ), 824 array( 'position', 'sticky' ), 825 array( 'spacing', 'blockGap' ), 826 array( 'spacing', 'margin' ), 827 array( 'spacing', 'padding' ), 828 array( 'typography', 'lineHeight' ), 829 array( 'typography', 'textColumns' ), 830 ); 831 832 /** 833 * The latest version of the schema in use. 834 * 835 * @since 5.8.0 836 * @since 5.9.0 Changed value from 1 to 2. 837 * @since 6.6.0 Changed value from 2 to 3. 838 * @var int 839 */ 840 const LATEST_SCHEMA = 3; 841 842 /** 843 * Constructor. 844 * 845 * @since 5.8.0 846 * @since 6.6.0 Key spacingScale by origin, and Pre-generate the spacingSizes from spacingScale. 847 * Added unwrapping of shared block style variations into block type variations if registered. 848 * 849 * @param array $theme_json A structure that follows the theme.json schema. 850 * @param string $origin Optional. What source of data this object represents. 851 * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'. 852 */ 853 public function __construct( $theme_json = array( 'version' => self::LATEST_SCHEMA ), $origin = 'theme' ) { 854 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { 855 $origin = 'theme'; 856 } 857 858 $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin ); 859 $blocks_metadata = static::get_blocks_metadata(); 860 $valid_block_names = array_keys( $blocks_metadata ); 861 $valid_element_names = array_keys( static::ELEMENTS ); 862 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata ); 863 $this->theme_json = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations ); 864 $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations ); 865 $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json ); 866 867 // Internally, presets are keyed by origin. 868 $nodes = static::get_setting_nodes( $this->theme_json ); 869 foreach ( $nodes as $node ) { 870 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 871 $path = $node['path']; 872 foreach ( $preset_metadata['path'] as $subpath ) { 873 $path[] = $subpath; 874 } 875 $preset = _wp_array_get( $this->theme_json, $path, null ); 876 if ( null !== $preset ) { 877 // If the preset is not already keyed by origin. 878 if ( isset( $preset[0] ) || empty( $preset ) ) { 879 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); 880 } 881 } 882 } 883 } 884 885 // In addition to presets, spacingScale (which generates presets) is also keyed by origin. 886 $scale_path = array( 'settings', 'spacing', 'spacingScale' ); 887 $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); 888 if ( null !== $spacing_scale ) { 889 // If the spacingScale is not already keyed by origin. 890 if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) { 891 _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) ); 892 } 893 } 894 895 // Pre-generate the spacingSizes from spacingScale. 896 $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); 897 $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); 898 if ( isset( $spacing_scale ) ) { 899 $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); 900 $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() ); 901 $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale ); 902 $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); 903 _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes ); 904 } 905 } 906 907 /** 908 * Unwraps shared block style variations. 909 * 910 * It takes the shared variations (styles.variations.variationName) and 911 * applies them to all the blocks that have the given variation registered 912 * (styles.blocks.blockType.variations.variationName). 913 * 914 * For example, given the `core/paragraph` and `core/group` blocks have 915 * registered the `section-a` style variation, and given the following input: 916 * 917 * { 918 * "styles": { 919 * "variations": { 920 * "section-a": { "color": { "background": "backgroundColor" } } 921 * } 922 * } 923 * } 924 * 925 * It returns the following output: 926 * 927 * { 928 * "styles": { 929 * "blocks": { 930 * "core/paragraph": { 931 * "variations": { 932 * "section-a": { "color": { "background": "backgroundColor" } } 933 * }, 934 * }, 935 * "core/group": { 936 * "variations": { 937 * "section-a": { "color": { "background": "backgroundColor" } } 938 * } 939 * } 940 * } 941 * } 942 * } 943 * 944 * @since 6.6.0 945 * 946 * @param array $theme_json A structure that follows the theme.json schema. 947 * @param array $valid_variations Valid block style variations. 948 * @return array Theme json data with shared variation definitions unwrapped under appropriate block types. 949 */ 950 private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) { 951 if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) { 952 return $theme_json; 953 } 954 955 $new_theme_json = $theme_json; 956 $variations = $new_theme_json['styles']['variations']; 957 958 foreach ( $valid_variations as $block_type => $registered_variations ) { 959 foreach ( $registered_variations as $variation_name ) { 960 $block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); 961 $top_level_data = $variations[ $variation_name ] ?? array(); 962 $merged_data = array_replace_recursive( $top_level_data, $block_level_data ); 963 if ( ! empty( $merged_data ) ) { 964 _wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data ); 965 } 966 } 967 } 968 969 unset( $new_theme_json['styles']['variations'] ); 970 971 return $new_theme_json; 972 } 973 974 /** 975 * Enables some opt-in settings if theme declared support. 976 * 977 * @since 5.9.0 978 * 979 * @param array $theme_json A theme.json structure to modify. 980 * @return array The modified theme.json structure. 981 */ 982 protected static function maybe_opt_in_into_settings( $theme_json ) { 983 $new_theme_json = $theme_json; 984 985 if ( 986 isset( $new_theme_json['settings']['appearanceTools'] ) && 987 true === $new_theme_json['settings']['appearanceTools'] 988 ) { 989 static::do_opt_in_into_settings( $new_theme_json['settings'] ); 990 } 991 992 if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { 993 foreach ( $new_theme_json['settings']['blocks'] as &$block ) { 994 if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { 995 static::do_opt_in_into_settings( $block ); 996 } 997 } 998 } 999 1000 return $new_theme_json; 1001 } 1002 1003 /** 1004 * Enables some settings. 1005 * 1006 * @since 5.9.0 1007 * 1008 * @param array $context The context to which the settings belong. 1009 */ 1010 protected static function do_opt_in_into_settings( &$context ) { 1011 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) { 1012 /* 1013 * Use "unset prop" as a marker instead of "null" because 1014 * "null" can be a valid value for some props (e.g. blockGap). 1015 */ 1016 if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { 1017 _wp_array_set( $context, $path, true ); 1018 } 1019 } 1020 1021 unset( $context['appearanceTools'] ); 1022 } 1023 1024 /** 1025 * Sanitizes the input according to the schemas. 1026 * 1027 * @since 5.8.0 1028 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. 1029 * @since 6.3.0 Added the `$valid_variations` parameter. 1030 * @since 6.6.0 Updated schema to allow extended block style variations. 1031 * 1032 * @param array $input Structure to sanitize. 1033 * @param array $valid_block_names List of valid block names. 1034 * @param array $valid_element_names List of valid element names. 1035 * @param array $valid_variations List of valid variations per block. 1036 * @return array The sanitized output. 1037 */ 1038 protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) { 1039 $output = array(); 1040 1041 if ( ! is_array( $input ) ) { 1042 return $output; 1043 } 1044 1045 // Preserve only the top most level keys. 1046 $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); 1047 1048 /* 1049 * Remove any rules that are annotated as "top" in VALID_STYLES constant. 1050 * Some styles are only meant to be available at the top-level (e.g.: blockGap), 1051 * hence, the schema for blocks & elements should not have them. 1052 */ 1053 $styles_non_top_level = static::VALID_STYLES; 1054 foreach ( array_keys( $styles_non_top_level ) as $section ) { 1055 // array_key_exists() needs to be used instead of isset() because the value can be null. 1056 if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { 1057 foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { 1058 if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { 1059 unset( $styles_non_top_level[ $section ][ $prop ] ); 1060 } 1061 } 1062 } 1063 } 1064 1065 // Build the schema based on valid block & element names. 1066 $schema = array(); 1067 $schema_styles_elements = array(); 1068 1069 /* 1070 * Set allowed element pseudo selectors and responsive breakpoint states. 1071 * Target data structure in schema: 1072 * e.g. 1073 * - top level elements: `$schema['styles']['elements']['link'][':hover']`. 1074 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. 1075 * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`. 1076 */ 1077 foreach ( $valid_element_names as $element ) { 1078 $schema_styles_elements[ $element ] = $styles_non_top_level; 1079 1080 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { 1081 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 1082 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; 1083 } 1084 } 1085 1086 // Add responsive breakpoint states for elements. 1087 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1088 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level; 1089 } 1090 } 1091 1092 $schema_styles_blocks = array(); 1093 $schema_settings_blocks = array(); 1094 1095 /* 1096 * Generate a schema for blocks. 1097 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions. 1098 * - Variations definitions cannot be nested. 1099 * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states. 1100 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states. 1101 * 1102 * As each variation needs both a `blocks` schema and responsive `blocks` schemas 1103 * for further nested inner `blocks`, the overall schema is generated in multiple passes. 1104 */ 1105 foreach ( $valid_block_names as $block ) { 1106 $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; 1107 $schema_styles_blocks[ $block ] = $styles_non_top_level; 1108 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; 1109 1110 // Add responsive breakpoint states for all blocks. 1111 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1112 $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level; 1113 $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements; 1114 1115 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1116 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1117 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; 1118 } 1119 } 1120 } 1121 1122 // Add pseudo-selectors for blocks that support them 1123 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1124 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1125 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level; 1126 } 1127 } 1128 } 1129 1130 $block_style_variation_styles = static::VALID_STYLES; 1131 $block_style_variation_styles['blocks'] = $schema_styles_blocks; 1132 $block_style_variation_styles['elements'] = $schema_styles_elements; 1133 1134 foreach ( $valid_block_names as $block ) { 1135 // Build the schema for each block style variation. 1136 $style_variation_names = array(); 1137 if ( 1138 ! empty( $input['styles']['blocks'][ $block ]['variations'] ) && 1139 is_array( $input['styles']['blocks'][ $block ]['variations'] ) && 1140 isset( $valid_variations[ $block ] ) 1141 ) { 1142 $style_variation_names = array_intersect( 1143 array_keys( $input['styles']['blocks'][ $block ]['variations'] ), 1144 $valid_variations[ $block ] 1145 ); 1146 } 1147 1148 $schema_styles_variations = array(); 1149 if ( ! empty( $style_variation_names ) ) { 1150 foreach ( $style_variation_names as $variation_name ) { 1151 $variation_schema = $block_style_variation_styles; 1152 1153 // Add responsive breakpoint states to block style variations. 1154 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1155 $variation_schema[ $breakpoint_state ] = $styles_non_top_level; 1156 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements; 1157 $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks; 1158 1159 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1160 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1161 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; 1162 } 1163 } 1164 } 1165 1166 // Add pseudo-selectors to variations for blocks that support them 1167 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1168 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1169 $variation_schema[ $pseudo_selector ] = $styles_non_top_level; 1170 } 1171 } 1172 1173 $schema_styles_variations[ $variation_name ] = $variation_schema; 1174 } 1175 } 1176 1177 $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; 1178 } 1179 1180 $schema['styles'] = static::VALID_STYLES; 1181 $schema['styles']['blocks'] = $schema_styles_blocks; 1182 $schema['styles']['elements'] = $schema_styles_elements; 1183 $schema['settings'] = static::VALID_SETTINGS; 1184 $schema['settings']['blocks'] = $schema_settings_blocks; 1185 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); 1186 1187 // Remove anything that's not present in the schema. 1188 foreach ( array( 'styles', 'settings' ) as $subtree ) { 1189 if ( ! isset( $input[ $subtree ] ) ) { 1190 continue; 1191 } 1192 1193 if ( ! is_array( $input[ $subtree ] ) ) { 1194 unset( $output[ $subtree ] ); 1195 continue; 1196 } 1197 1198 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); 1199 1200 if ( empty( $result ) ) { 1201 unset( $output[ $subtree ] ); 1202 } else { 1203 $output[ $subtree ] = static::resolve_custom_css_format( $result ); 1204 } 1205 } 1206 1207 return $output; 1208 } 1209 1210 /** 1211 * Appends a sub-selector to an existing one. 1212 * 1213 * Given the compounded $selector "h1, h2, h3" 1214 * and the $to_append selector ".some-class" the result will be 1215 * "h1.some-class, h2.some-class, h3.some-class". 1216 * 1217 * @since 5.8.0 1218 * @since 6.1.0 Added append position. 1219 * @since 6.3.0 Removed append position parameter. 1220 * 1221 * @param string $selector Original selector. 1222 * @param string $to_append Selector to append. 1223 * @return string The new selector. 1224 */ 1225 protected static function append_to_selector( $selector, $to_append ) { 1226 if ( ! str_contains( $selector, ',' ) ) { 1227 return $selector . $to_append; 1228 } 1229 $new_selectors = array(); 1230 $selectors = explode( ',', $selector ); 1231 foreach ( $selectors as $sel ) { 1232 $new_selectors[] = $sel . $to_append; 1233 } 1234 return implode( ',', $new_selectors ); 1235 } 1236 1237 /** 1238 * Prepends a sub-selector to an existing one. 1239 * 1240 * Given the compounded $selector "h1, h2, h3" 1241 * and the $to_prepend selector ".some-class " the result will be 1242 * ".some-class h1, .some-class h2, .some-class h3". 1243 * 1244 * @since 6.3.0 1245 * 1246 * @param string $selector Original selector. 1247 * @param string $to_prepend Selector to prepend. 1248 * @return string The new selector. 1249 */ 1250 protected static function prepend_to_selector( $selector, $to_prepend ) { 1251 if ( ! str_contains( $selector, ',' ) ) { 1252 return $to_prepend . $selector; 1253 } 1254 $new_selectors = array(); 1255 $selectors = explode( ',', $selector ); 1256 foreach ( $selectors as $sel ) { 1257 $new_selectors[] = $to_prepend . $sel; 1258 } 1259 return implode( ',', $new_selectors ); 1260 } 1261 1262 /** 1263 * Returns the metadata for each block. 1264 * 1265 * Example: 1266 * 1267 * { 1268 * 'core/paragraph': { 1269 * 'selector': 'p', 1270 * 'elements': { 1271 * 'link' => 'link selector', 1272 * 'etc' => 'element selector' 1273 * } 1274 * }, 1275 * 'core/heading': { 1276 * 'selector': 'h1', 1277 * 'elements': {} 1278 * }, 1279 * 'core/image': { 1280 * 'selector': '.wp-block-image', 1281 * 'duotone': 'img', 1282 * 'elements': {} 1283 * } 1284 * } 1285 * 1286 * @since 5.8.0 1287 * @since 5.9.0 Added `duotone` key with CSS selector. 1288 * @since 6.1.0 Added `features` key with block support feature level selectors. 1289 * @since 6.3.0 Refactored and stabilized selectors API. 1290 * @since 6.6.0 Updated to include block style variations from the block styles registry. 1291 * 1292 * @return array Block metadata. 1293 */ 1294 protected static function get_blocks_metadata() { 1295 $registry = WP_Block_Type_Registry::get_instance(); 1296 $blocks = $registry->get_all_registered(); 1297 $style_registry = WP_Block_Styles_Registry::get_instance(); 1298 1299 // Is there metadata for all currently registered blocks? 1300 $blocks = array_diff_key( $blocks, static::$blocks_metadata ); 1301 if ( empty( $blocks ) ) { 1302 /* 1303 * New block styles may have been registered within WP_Block_Styles_Registry. 1304 * Update block metadata for any new block style variations. 1305 */ 1306 $registered_styles = $style_registry->get_all_registered(); 1307 foreach ( static::$blocks_metadata as $block_name => $block_metadata ) { 1308 if ( ! empty( $registered_styles[ $block_name ] ) ) { 1309 $style_selectors = $block_metadata['styleVariations'] ?? array(); 1310 1311 foreach ( $registered_styles[ $block_name ] as $block_style ) { 1312 if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) { 1313 $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] ); 1314 } 1315 } 1316 1317 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; 1318 } 1319 } 1320 return static::$blocks_metadata; 1321 } 1322 1323 foreach ( $blocks as $block_name => $block_type ) { 1324 $root_selector = wp_get_block_css_selector( $block_type ); 1325 1326 static::$blocks_metadata[ $block_name ]['selector'] = $root_selector; 1327 static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector ); 1328 1329 $elements = static::get_block_element_selectors( $root_selector ); 1330 if ( ! empty( $elements ) ) { 1331 static::$blocks_metadata[ $block_name ]['elements'] = $elements; 1332 } 1333 1334 // The block may or may not have a duotone selector. 1335 $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' ); 1336 1337 // Keep backwards compatibility for support.color.__experimentalDuotone. 1338 if ( null === $duotone_selector ) { 1339 $duotone_support = $block_type->supports['color']['__experimentalDuotone'] ?? null; 1340 1341 if ( $duotone_support ) { 1342 $root_selector = wp_get_block_css_selector( $block_type ); 1343 $duotone_selector = static::scope_selector( $root_selector, $duotone_support ); 1344 } 1345 } 1346 1347 if ( null !== $duotone_selector ) { 1348 static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; 1349 } 1350 1351 // If the block has style variations, append their selectors to the block metadata. 1352 $style_selectors = array(); 1353 if ( ! empty( $block_type->styles ) ) { 1354 foreach ( $block_type->styles as $style ) { 1355 $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); 1356 } 1357 } 1358 1359 // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json. 1360 $registered_styles = $style_registry->get_registered_styles_for_block( $block_name ); 1361 foreach ( $registered_styles as $style ) { 1362 $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); 1363 } 1364 1365 if ( ! empty( $style_selectors ) ) { 1366 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; 1367 } 1368 } 1369 1370 return static::$blocks_metadata; 1371 } 1372 1373 /** 1374 * Given a tree, removes the keys that are not present in the schema. 1375 * 1376 * It is recursive and modifies the input in-place. 1377 * 1378 * @since 5.8.0 1379 * @since 7.0.0 Added type validation for boolean values. 1380 * 1381 * @param array $tree Input to process. 1382 * @param array $schema Schema to adhere to. 1383 * @return array The modified $tree. 1384 */ 1385 protected static function remove_keys_not_in_schema( $tree, $schema ) { 1386 if ( ! is_array( $tree ) ) { 1387 return $tree; 1388 } 1389 1390 foreach ( $tree as $key => $value ) { 1391 // Remove keys not in the schema or with null/empty values. 1392 if ( ! array_key_exists( $key, $schema ) ) { 1393 unset( $tree[ $key ] ); 1394 continue; 1395 } 1396 1397 // Validate type if schema specifies a boolean marker. 1398 if ( is_bool( $schema[ $key ] ) ) { 1399 // Schema expects a boolean value - validate the input matches. 1400 if ( ! is_bool( $value ) ) { 1401 unset( $tree[ $key ] ); 1402 continue; 1403 } 1404 // Type matches, keep the value and continue to next key. 1405 continue; 1406 } 1407 1408 if ( is_array( $schema[ $key ] ) ) { 1409 if ( ! is_array( $value ) ) { 1410 unset( $tree[ $key ] ); 1411 } elseif ( wp_is_numeric_array( $value ) ) { 1412 // If indexed, process each item in the array. 1413 foreach ( $value as $item_key => $item_value ) { 1414 if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) { 1415 $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] ); 1416 } else { 1417 // If the schema does not define a further structure, keep the value as is. 1418 $tree[ $key ][ $item_key ] = $item_value; 1419 } 1420 } 1421 } else { 1422 // If associative, process as a single object. 1423 $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] ); 1424 1425 if ( empty( $tree[ $key ] ) ) { 1426 unset( $tree[ $key ] ); 1427 } 1428 } 1429 } 1430 } 1431 return $tree; 1432 } 1433 1434 /** 1435 * Returns the existing settings for each block. 1436 * 1437 * Example: 1438 * 1439 * { 1440 * 'root': { 1441 * 'color': { 1442 * 'custom': true 1443 * } 1444 * }, 1445 * 'core/paragraph': { 1446 * 'spacing': { 1447 * 'customPadding': true 1448 * } 1449 * } 1450 * } 1451 * 1452 * @since 5.8.0 1453 * 1454 * @return array Settings per block. 1455 */ 1456 public function get_settings() { 1457 if ( ! isset( $this->theme_json['settings'] ) ) { 1458 return array(); 1459 } else { 1460 return $this->theme_json['settings']; 1461 } 1462 } 1463 1464 /** 1465 * Returns the stylesheet that results of processing 1466 * the theme.json structure this object represents. 1467 * 1468 * @since 5.8.0 1469 * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters. 1470 * @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available. 1471 * @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options 1472 * to control styles output as desired. 1473 * @since 7.0.0 Deprecated 'base-layout-styles' type; added `base_layout_styles` option for classic themes. 1474 * 1475 * @param string[] $types Types of styles to load. Will load all by default. It accepts: 1476 * - `variables`: only the CSS Custom Properties for presets & custom ones. 1477 * - `styles`: only the styles section in theme.json. 1478 * - `presets`: only the classes for the presets. 1479 * - `base-layout-styles`: only the base layout styles. Deprecated in 7.0.0. 1480 * - `custom-css`: only the custom CSS. 1481 * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS. 1482 * @param array $options { 1483 * Optional. An array of options for now used for internal purposes only (may change without notice). 1484 * 1485 * @type string $scope Makes sure all style are scoped to a given selector 1486 * @type string $root_selector Overwrites and forces a given selector to be used on the root node 1487 * @type bool $skip_root_layout_styles Omits root layout styles from the generated stylesheet. Default false. 1488 * @type bool $base_layout_styles When true generates only base layout styles without alignment rules. Default false. 1489 * @type bool $include_block_style_variations Includes styles for block style variations in the generated stylesheet. Default false. 1490 * } 1491 * @return string The resulting stylesheet. 1492 */ 1493 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { 1494 if ( null === $origins ) { 1495 $origins = static::VALID_ORIGINS; 1496 } 1497 1498 if ( is_string( $types ) ) { 1499 // Dispatch error and map old arguments to new ones. 1500 _deprecated_argument( __FUNCTION__, '5.9.0' ); 1501 if ( 'block_styles' === $types ) { 1502 $types = array( 'styles', 'presets' ); 1503 } elseif ( 'css_variables' === $types ) { 1504 $types = array( 'variables' ); 1505 } else { 1506 $types = array( 'variables', 'styles', 'presets' ); 1507 } 1508 } 1509 1510 $blocks_metadata = static::get_blocks_metadata(); 1511 $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options ); 1512 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); 1513 1514 $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); 1515 $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); 1516 1517 if ( ! empty( $options['scope'] ) ) { 1518 foreach ( $setting_nodes as &$node ) { 1519 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); 1520 } 1521 foreach ( $style_nodes as &$node ) { 1522 $node = static::scope_style_node_selectors( $options['scope'], $node ); 1523 } 1524 unset( $node ); 1525 } 1526 1527 if ( ! empty( $options['root_selector'] ) ) { 1528 if ( false !== $root_settings_key ) { 1529 $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; 1530 } 1531 if ( false !== $root_style_key ) { 1532 $style_nodes[ $root_style_key ]['selector'] = $options['root_selector']; 1533 } 1534 } 1535 1536 $stylesheet = ''; 1537 1538 if ( in_array( 'variables', $types, true ) ) { 1539 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); 1540 } 1541 1542 if ( in_array( 'styles', $types, true ) ) { 1543 if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) { 1544 $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ], $options ); 1545 } 1546 $stylesheet .= $this->get_block_classes( $style_nodes ); 1547 } 1548 1549 if ( in_array( 'presets', $types, true ) ) { 1550 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); 1551 } 1552 1553 // Load the custom CSS last so it has the highest specificity. 1554 if ( in_array( 'custom-css', $types, true ) ) { 1555 // Add the global styles root CSS. 1556 $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); 1557 } 1558 1559 return $stylesheet; 1560 } 1561 1562 /** 1563 * Processes the CSS, to apply nesting. 1564 * 1565 * @since 6.2.0 1566 * @since 6.6.0 Enforced 0-1-0 specificity for block custom CSS selectors. 1567 * @since 7.0.0 Made public for use in custom-css block support. 1568 * 1569 * @param string $css The CSS to process. 1570 * @param string $selector The selector to nest. 1571 * @return string The processed CSS. 1572 */ 1573 public static function process_blocks_custom_css( $css, $selector ) { 1574 $processed_css = ''; 1575 1576 if ( empty( $css ) ) { 1577 return $processed_css; 1578 } 1579 1580 // Split CSS nested rules. 1581 $parts = explode( '&', $css ); 1582 foreach ( $parts as $part ) { 1583 if ( empty( $part ) ) { 1584 continue; 1585 } 1586 $is_root_css = ( ! str_contains( $part, '{' ) ); 1587 if ( $is_root_css ) { 1588 // If the part doesn't contain braces, it applies to the root level. 1589 $processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}'; 1590 } else { 1591 // If the part contains braces, it's a nested CSS rule. 1592 $part = explode( '{', str_replace( '}', '', $part ) ); 1593 if ( count( $part ) !== 2 ) { 1594 continue; 1595 } 1596 $nested_selector = $part[0]; 1597 $css_value = $part[1]; 1598 1599 /* 1600 * Handle pseudo elements such as ::before, ::after etc. Regex will also 1601 * capture any leading combinator such as >, +, or ~, as well as spaces. 1602 * This allows pseudo elements as descendants e.g. `.parent ::before`. 1603 */ 1604 $matches = array(); 1605 $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches ); 1606 $pseudo_part = $has_pseudo_element ? $matches[1] : ''; 1607 $nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector; 1608 1609 // Finalize selector and re-append pseudo element if required. 1610 $part_selector = str_starts_with( $nested_selector, ' ' ) 1611 ? static::scope_selector( $selector, $nested_selector ) 1612 : static::append_to_selector( $selector, $nested_selector ); 1613 $final_selector = ":root :where($part_selector)$pseudo_part"; 1614 1615 $processed_css .= $final_selector . '{' . trim( $css_value ) . '}'; 1616 } 1617 } 1618 return $processed_css; 1619 } 1620 1621 /** 1622 * Returns the global styles custom CSS. 1623 * 1624 * @since 6.2.0 1625 * @deprecated 6.7.0 Use {@see 'get_stylesheet'} instead. 1626 * 1627 * @return string The global styles custom CSS. 1628 */ 1629 public function get_custom_css() { 1630 _deprecated_function( __METHOD__, '6.7.0', 'get_stylesheet' ); 1631 // Add the global styles root CSS. 1632 $stylesheet = $this->theme_json['styles']['css'] ?? ''; 1633 1634 // Add the global styles block CSS. 1635 if ( isset( $this->theme_json['styles']['blocks'] ) ) { 1636 foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) { 1637 $custom_block_css = $this->theme_json['styles']['blocks'][ $name ]['css'] ?? null; 1638 if ( $custom_block_css ) { 1639 $selector = static::$blocks_metadata[ $name ]['selector']; 1640 $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector ); 1641 } 1642 } 1643 } 1644 1645 return $stylesheet; 1646 } 1647 1648 /** 1649 * Returns the page templates of the active theme. 1650 * 1651 * @since 5.9.0 1652 * 1653 * @return array 1654 */ 1655 public function get_custom_templates() { 1656 $custom_templates = array(); 1657 if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { 1658 return $custom_templates; 1659 } 1660 1661 foreach ( $this->theme_json['customTemplates'] as $item ) { 1662 if ( isset( $item['name'] ) ) { 1663 $custom_templates[ $item['name'] ] = array( 1664 'title' => $item['title'] ?? '', 1665 'postTypes' => $item['postTypes'] ?? array( 'page' ), 1666 ); 1667 } 1668 } 1669 return $custom_templates; 1670 } 1671 1672 /** 1673 * Returns the template part data of active theme. 1674 * 1675 * @since 5.9.0 1676 * 1677 * @return array 1678 */ 1679 public function get_template_parts() { 1680 $template_parts = array(); 1681 if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { 1682 return $template_parts; 1683 } 1684 1685 foreach ( $this->theme_json['templateParts'] as $item ) { 1686 if ( isset( $item['name'] ) ) { 1687 $template_parts[ $item['name'] ] = array( 1688 'title' => $item['title'] ?? '', 1689 'area' => $item['area'] ?? '', 1690 ); 1691 } 1692 } 1693 return $template_parts; 1694 } 1695 1696 /** 1697 * Converts each style section into a list of rulesets 1698 * containing the block styles to be appended to the stylesheet. 1699 * 1700 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax 1701 * 1702 * For each section this creates a new ruleset such as: 1703 * 1704 * block-selector { 1705 * style-property-one: value; 1706 * } 1707 * 1708 * @since 5.8.0 As `get_block_styles()`. 1709 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()` 1710 * and no longer returns preset classes. 1711 * Removed the `$setting_nodes` parameter. 1712 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`. 1713 * 1714 * @param array $style_nodes Nodes with styles. 1715 * @return string The new stylesheet. 1716 */ 1717 protected function get_block_classes( $style_nodes ) { 1718 $block_rules = ''; 1719 1720 foreach ( $style_nodes as $metadata ) { 1721 if ( null === $metadata['selector'] ) { 1722 continue; 1723 } 1724 $block_rules .= static::get_styles_for_block( $metadata ); 1725 } 1726 1727 return $block_rules; 1728 } 1729 1730 /** 1731 * Gets the CSS layout rules for a particular block from theme.json layout definitions. 1732 * 1733 * @since 6.1.0 1734 * @since 6.3.0 Reduced specificity for layout margin rules. 1735 * @since 6.5.1 Only output rules referencing content and wide sizes when values exist. 1736 * @since 6.5.3 Add types parameter to check if only base layout styles are needed. 1737 * @since 6.6.0 Updated layout style specificity to be compatible with overall 0-1-0 specificity in global styles. 1738 * @since 7.0.0 Replaced `$types` parameter with `$options` array; base layout styles controlled via `base_layout_styles` option. 1739 * 1740 * @param array $block_metadata Metadata about the block to get styles for. 1741 * @param array $options Optional. An array of options for now used for internal purposes only. 1742 * @return string Layout styles for the block. 1743 */ 1744 protected function get_layout_styles( $block_metadata, $options = array() ) { 1745 $block_rules = ''; 1746 $block_type = null; 1747 1748 // Skip outputting layout styles if explicitly disabled. 1749 if ( current_theme_supports( 'disable-layout-styles' ) ) { 1750 return $block_rules; 1751 } 1752 1753 if ( isset( $block_metadata['name'] ) ) { 1754 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); 1755 if ( ! block_has_support( $block_type, 'layout', false ) && ! block_has_support( $block_type, '__experimentalLayout', false ) ) { 1756 return $block_rules; 1757 } 1758 } 1759 1760 $selector = $block_metadata['selector'] ?? ''; 1761 $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] ); 1762 $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. 1763 $node = $options['node'] ?? _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); 1764 $layout_definitions = wp_get_layout_definitions(); 1765 $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. 1766 1767 /* 1768 * Gap styles will only be output if the theme has block gap support, or supports a fallback gap. 1769 * Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. 1770 */ 1771 if ( $has_block_gap_support || $has_fallback_gap_support ) { 1772 $block_gap_value = null; 1773 // Use a fallback gap value if block gap support is not available. 1774 if ( ! $has_block_gap_support ) { 1775 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; 1776 if ( ! empty( $block_type ) ) { 1777 $block_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null; 1778 } 1779 } else { 1780 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); 1781 } 1782 1783 // Support split row / column values and concatenate to a shorthand value. 1784 if ( is_array( $block_gap_value ) ) { 1785 if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { 1786 $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); 1787 $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); 1788 $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; 1789 } else { 1790 // Skip outputting gap value if not all sides are provided. 1791 $block_gap_value = null; 1792 } 1793 } 1794 1795 // If the block should have custom gap, add the gap styles. 1796 if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { 1797 foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { 1798 // Allow outputting fallback gap styles for flex and grid layout types when block gap support isn't available. 1799 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) { 1800 continue; 1801 } 1802 1803 $class_name = $layout_definition['className'] ?? false; 1804 $spacing_rules = $layout_definition['spacingStyles'] ?? array(); 1805 1806 if ( 1807 ! empty( $class_name ) && 1808 ! empty( $spacing_rules ) 1809 ) { 1810 foreach ( $spacing_rules as $spacing_rule ) { 1811 $declarations = array(); 1812 if ( 1813 isset( $spacing_rule['selector'] ) && 1814 preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && 1815 ! empty( $spacing_rule['rules'] ) 1816 ) { 1817 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. 1818 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { 1819 $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; 1820 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { 1821 $declarations[] = array( 1822 'name' => $css_property, 1823 'value' => $current_css_value, 1824 ); 1825 } 1826 } 1827 1828 if ( ! $has_block_gap_support ) { 1829 // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. 1830 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; 1831 $layout_selector = sprintf( 1832 $format, 1833 $selector, 1834 $class_name, 1835 $spacing_rule['selector'] 1836 ); 1837 } else { 1838 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s'; 1839 $layout_selector = sprintf( 1840 $format, 1841 $selector, 1842 $class_name, 1843 $spacing_rule['selector'] 1844 ); 1845 } 1846 $block_rules .= static::to_ruleset( $layout_selector, $declarations ); 1847 } 1848 } 1849 } 1850 } 1851 } 1852 } 1853 1854 // Output base styles. 1855 if ( 1856 static::ROOT_BLOCK_SELECTOR === $selector 1857 ) { 1858 $valid_display_modes = array( 'block', 'flex', 'grid' ); 1859 foreach ( $layout_definitions as $layout_definition ) { 1860 $class_name = $layout_definition['className'] ?? false; 1861 $base_style_rules = $layout_definition['baseStyles'] ?? array(); 1862 1863 if ( 1864 ! empty( $class_name ) && 1865 is_array( $base_style_rules ) 1866 ) { 1867 // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. 1868 if ( 1869 ! empty( $layout_definition['displayMode'] ) && 1870 is_string( $layout_definition['displayMode'] ) && 1871 in_array( $layout_definition['displayMode'], $valid_display_modes, true ) 1872 ) { 1873 $layout_selector = sprintf( 1874 '%s .%s', 1875 $selector, 1876 $class_name 1877 ); 1878 $block_rules .= static::to_ruleset( 1879 $layout_selector, 1880 array( 1881 array( 1882 'name' => 'display', 1883 'value' => $layout_definition['displayMode'], 1884 ), 1885 ) 1886 ); 1887 } 1888 1889 foreach ( $base_style_rules as $base_style_rule ) { 1890 $declarations = array(); 1891 1892 // Skip outputting base styles for flow and constrained layout types when base_layout_styles is enabled. 1893 // These themes don't use .wp-site-blocks wrapper, so these layout-specific alignment styles aren't needed. 1894 if ( ! empty( $options['base_layout_styles'] ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) { 1895 continue; 1896 } 1897 1898 if ( 1899 isset( $base_style_rule['selector'] ) && 1900 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && 1901 ! empty( $base_style_rule['rules'] ) 1902 ) { 1903 foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { 1904 // Skip rules that reference content size or wide size if they are not defined in the theme.json. 1905 if ( 1906 is_string( $css_value ) && 1907 ( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) && 1908 ! isset( $this->theme_json['settings']['layout']['contentSize'] ) && 1909 ! isset( $this->theme_json['settings']['layout']['wideSize'] ) 1910 ) { 1911 continue; 1912 } 1913 1914 if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { 1915 $declarations[] = array( 1916 'name' => $css_property, 1917 'value' => $css_value, 1918 ); 1919 } 1920 } 1921 1922 $layout_selector = sprintf( 1923 '.%s%s', 1924 $class_name, 1925 $base_style_rule['selector'] 1926 ); 1927 $block_rules .= static::to_ruleset( $layout_selector, $declarations ); 1928 } 1929 } 1930 } 1931 } 1932 } 1933 1934 if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) { 1935 $block_rules = $options['media_query'] . '{' . $block_rules . '}'; 1936 } 1937 1938 return $block_rules; 1939 } 1940 1941 /** 1942 * Creates new rulesets as classes for each preset value such as: 1943 * 1944 * .has-value-color { 1945 * color: value; 1946 * } 1947 * 1948 * .has-value-background-color { 1949 * background-color: value; 1950 * } 1951 * 1952 * .has-value-font-size { 1953 * font-size: value; 1954 * } 1955 * 1956 * .has-value-gradient-background { 1957 * background: value; 1958 * } 1959 * 1960 * p.has-value-gradient-background { 1961 * background: value; 1962 * } 1963 * 1964 * @since 5.9.0 1965 * 1966 * @param array $setting_nodes Nodes with settings. 1967 * @param string[] $origins List of origins to process presets from. 1968 * @return string The new stylesheet. 1969 */ 1970 protected function get_preset_classes( $setting_nodes, $origins ) { 1971 $preset_rules = ''; 1972 1973 foreach ( $setting_nodes as $metadata ) { 1974 if ( null === $metadata['selector'] ) { 1975 continue; 1976 } 1977 1978 $selector = $metadata['selector']; 1979 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 1980 $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); 1981 } 1982 1983 return $preset_rules; 1984 } 1985 1986 /** 1987 * Converts each styles section into a list of rulesets 1988 * to be appended to the stylesheet. 1989 * These rulesets contain all the css variables (custom variables and preset variables). 1990 * 1991 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax 1992 * 1993 * For each section this creates a new ruleset such as: 1994 * 1995 * block-selector { 1996 * --wp--preset--category--slug: value; 1997 * --wp--custom--variable: value; 1998 * } 1999 * 2000 * @since 5.8.0 2001 * @since 5.9.0 Added the `$origins` parameter. 2002 * 2003 * @param array $nodes Nodes with settings. 2004 * @param string[] $origins List of origins to process. 2005 * @return string The new stylesheet. 2006 */ 2007 protected function get_css_variables( $nodes, $origins ) { 2008 $stylesheet = ''; 2009 foreach ( $nodes as $metadata ) { 2010 if ( null === $metadata['selector'] ) { 2011 continue; 2012 } 2013 2014 $selector = $metadata['selector']; 2015 2016 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 2017 $declarations = static::compute_preset_vars( $node, $origins ); 2018 $theme_vars_declarations = static::compute_theme_vars( $node ); 2019 foreach ( $theme_vars_declarations as $theme_vars_declaration ) { 2020 $declarations[] = $theme_vars_declaration; 2021 } 2022 2023 $stylesheet .= static::to_ruleset( $selector, $declarations ); 2024 } 2025 2026 return $stylesheet; 2027 } 2028 2029 /** 2030 * Given a selector and a declaration list, 2031 * creates the corresponding ruleset. 2032 * 2033 * @since 5.8.0 2034 * @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.). 2035 * 2036 * @param string $selector CSS selector. 2037 * @param array $declarations List of declarations. 2038 * @return string The resulting CSS ruleset. 2039 */ 2040 protected static function to_ruleset( $selector, $declarations ) { 2041 if ( empty( $declarations ) ) { 2042 return ''; 2043 } 2044 2045 $declaration_block = array_reduce( 2046 $declarations, 2047 static function ( $carry, $element ) { 2048 $value = $element['value']; 2049 2050 if ( is_numeric( $value ) ) { 2051 $value = (string) $value; 2052 } 2053 2054 if ( ! is_string( $value ) ) { 2055 return $carry; 2056 } 2057 2058 return $carry .= $element['name'] . ': ' . $value . ';'; 2059 }, 2060 '' 2061 ); 2062 2063 return $selector . '{' . $declaration_block . '}'; 2064 } 2065 2066 /** 2067 * Given a settings array, returns the generated rulesets 2068 * for the preset classes. 2069 * 2070 * @since 5.8.0 2071 * @since 5.9.0 Added the `$origins` parameter. 2072 * @since 6.6.0 Added check for root CSS properties selector. 2073 * 2074 * @param array $settings Settings to process. 2075 * @param string $selector Selector wrapping the classes. 2076 * @param string[] $origins List of origins to process. 2077 * @return string The result of processing the presets. 2078 */ 2079 protected static function compute_preset_classes( $settings, $selector, $origins ) { 2080 if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) { 2081 /* 2082 * Classes at the global level do not need any CSS prefixed, 2083 * and we don't want to increase its specificity. 2084 */ 2085 $selector = ''; 2086 } 2087 2088 $stylesheet = ''; 2089 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 2090 if ( empty( $preset_metadata['classes'] ) ) { 2091 continue; 2092 } 2093 $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); 2094 foreach ( $preset_metadata['classes'] as $class => $property ) { 2095 foreach ( $slugs as $slug ) { 2096 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); 2097 $class_name = static::replace_slug_in_string( $class, $slug ); 2098 2099 // $selector is often empty, so we can save ourselves the `append_to_selector()` call then. 2100 $new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name ); 2101 $stylesheet .= static::to_ruleset( 2102 $new_selector, 2103 array( 2104 array( 2105 'name' => $property, 2106 'value' => 'var(' . $css_var . ') !important', 2107 ), 2108 ) 2109 ); 2110 } 2111 } 2112 } 2113 2114 return $stylesheet; 2115 } 2116 2117 /** 2118 * Function that scopes a selector with another one. This works a bit like 2119 * SCSS nesting except the `&` operator isn't supported. 2120 * 2121 * <code> 2122 * $scope = '.a, .b .c'; 2123 * $selector = '> .x, .y'; 2124 * $merged = scope_selector( $scope, $selector ); 2125 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' 2126 * </code> 2127 * 2128 * @since 5.9.0 2129 * @since 6.6.0 Added early return if missing scope or selector. 2130 * 2131 * @param string $scope Selector to scope to. 2132 * @param string $selector Original selector. 2133 * @return string Scoped selector. 2134 */ 2135 public static function scope_selector( $scope, $selector ) { 2136 if ( ! $scope || ! $selector ) { 2137 return $selector; 2138 } 2139 2140 $scopes = explode( ',', $scope ); 2141 $selectors = explode( ',', $selector ); 2142 2143 $selectors_scoped = array(); 2144 foreach ( $scopes as $outer ) { 2145 foreach ( $selectors as $inner ) { 2146 $outer = trim( $outer ); 2147 $inner = trim( $inner ); 2148 if ( ! empty( $outer ) && ! empty( $inner ) ) { 2149 $selectors_scoped[] = $outer . ' ' . $inner; 2150 } elseif ( empty( $outer ) ) { 2151 $selectors_scoped[] = $inner; 2152 } elseif ( empty( $inner ) ) { 2153 $selectors_scoped[] = $outer; 2154 } 2155 } 2156 } 2157 2158 $result = implode( ', ', $selectors_scoped ); 2159 return $result; 2160 } 2161 2162 /** 2163 * Scopes the selectors for a given style node. 2164 * 2165 * This includes the primary selector, i.e. `$node['selector']`, as well as any custom 2166 * selectors for features and subfeatures, e.g. `$node['selectors']['border']` etc. 2167 * 2168 * @since 6.6.0 2169 * 2170 * @param string $scope Selector to scope to. 2171 * @param array $node Style node with selectors to scope. 2172 * @return array Node with updated selectors. 2173 */ 2174 protected static function scope_style_node_selectors( $scope, $node ) { 2175 $node['selector'] = static::scope_selector( $scope, $node['selector'] ); 2176 2177 if ( empty( $node['selectors'] ) ) { 2178 return $node; 2179 } 2180 2181 foreach ( $node['selectors'] as $feature => $selector ) { 2182 if ( is_string( $selector ) ) { 2183 $node['selectors'][ $feature ] = static::scope_selector( $scope, $selector ); 2184 } 2185 if ( is_array( $selector ) ) { 2186 foreach ( $selector as $subfeature => $subfeature_selector ) { 2187 $node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector ); 2188 } 2189 } 2190 } 2191 2192 return $node; 2193 } 2194 2195 /** 2196 * Gets preset values keyed by slugs based on settings and metadata. 2197 * 2198 * <code> 2199 * $settings = array( 2200 * 'typography' => array( 2201 * 'fontFamilies' => array( 2202 * array( 2203 * 'slug' => 'sansSerif', 2204 * 'fontFamily' => '"Helvetica Neue", sans-serif', 2205 * ), 2206 * array( 2207 * 'slug' => 'serif', 2208 * 'colors' => 'Georgia, serif', 2209 * ) 2210 * ), 2211 * ), 2212 * ); 2213 * $meta = array( 2214 * 'path' => array( 'typography', 'fontFamilies' ), 2215 * 'value_key' => 'fontFamily', 2216 * ); 2217 * $values_by_slug = get_settings_values_by_slug(); 2218 * // $values_by_slug === array( 2219 * // 'sans-serif' => '"Helvetica Neue", sans-serif', 2220 * // 'serif' => 'Georgia, serif', 2221 * // ); 2222 * </code> 2223 * 2224 * @since 5.9.0 2225 * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA. 2226 * 2227 * @param array $settings Settings to process. 2228 * @param array $preset_metadata One of the PRESETS_METADATA values. 2229 * @param string[] $origins List of origins to process. 2230 * @return array Array of presets where each key is a slug and each value is the preset value. 2231 */ 2232 protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { 2233 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 2234 2235 $result = array(); 2236 foreach ( $origins as $origin ) { 2237 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 2238 continue; 2239 } 2240 foreach ( $preset_per_origin[ $origin ] as $preset ) { 2241 $slug = _wp_to_kebab_case( $preset['slug'] ); 2242 2243 $value = ''; 2244 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { 2245 $value_key = $preset_metadata['value_key']; 2246 $value = $preset[ $value_key ]; 2247 } elseif ( 2248 isset( $preset_metadata['value_func'] ) && 2249 is_callable( $preset_metadata['value_func'] ) 2250 ) { 2251 $value_func = $preset_metadata['value_func']; 2252 $value = call_user_func( $value_func, $preset, $settings ); 2253 } else { 2254 // If we don't have a value, then don't add it to the result. 2255 continue; 2256 } 2257 2258 $result[ $slug ] = $value; 2259 } 2260 } 2261 return $result; 2262 } 2263 2264 /** 2265 * Similar to get_settings_values_by_slug, but doesn't compute the value. 2266 * 2267 * @since 5.9.0 2268 * 2269 * @param array $settings Settings to process. 2270 * @param array $preset_metadata One of the PRESETS_METADATA values. 2271 * @param string[] $origins List of origins to process. 2272 * @return array Array of presets where the key and value are both the slug. 2273 */ 2274 protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { 2275 if ( null === $origins ) { 2276 $origins = static::VALID_ORIGINS; 2277 } 2278 2279 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 2280 2281 $result = array(); 2282 foreach ( $origins as $origin ) { 2283 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 2284 continue; 2285 } 2286 foreach ( $preset_per_origin[ $origin ] as $preset ) { 2287 $slug = _wp_to_kebab_case( $preset['slug'] ); 2288 2289 // Use the array as a set so we don't get duplicates. 2290 $result[ $slug ] = $slug; 2291 } 2292 } 2293 return $result; 2294 } 2295 2296 /** 2297 * Transforms a slug into a CSS Custom Property. 2298 * 2299 * @since 5.9.0 2300 * 2301 * @param string $input String to replace. 2302 * @param string $slug The slug value to use to generate the custom property. 2303 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. 2304 */ 2305 protected static function replace_slug_in_string( $input, $slug ) { 2306 return strtr( $input, array( '$slug' => $slug ) ); 2307 } 2308 2309 /** 2310 * Given the block settings, extracts the CSS Custom Properties 2311 * for the presets and adds them to the $declarations array 2312 * following the format: 2313 * 2314 * array( 2315 * 'name' => 'property_name', 2316 * 'value' => 'property_value, 2317 * ) 2318 * 2319 * @since 5.8.0 2320 * @since 5.9.0 Added the `$origins` parameter. 2321 * 2322 * @param array $settings Settings to process. 2323 * @param string[] $origins List of origins to process. 2324 * @return array The modified $declarations. 2325 */ 2326 protected static function compute_preset_vars( $settings, $origins ) { 2327 $declarations = array(); 2328 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 2329 if ( empty( $preset_metadata['css_vars'] ) ) { 2330 continue; 2331 } 2332 $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); 2333 foreach ( $values_by_slug as $slug => $value ) { 2334 $declarations[] = array( 2335 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), 2336 'value' => $value, 2337 ); 2338 } 2339 } 2340 2341 return $declarations; 2342 } 2343 2344 /** 2345 * Given an array of settings, extracts the CSS Custom Properties 2346 * for the custom values and adds them to the $declarations 2347 * array following the format: 2348 * 2349 * array( 2350 * 'name' => 'property_name', 2351 * 'value' => 'property_value, 2352 * ) 2353 * 2354 * @since 5.8.0 2355 * 2356 * @param array $settings Settings to process. 2357 * @return array The modified $declarations. 2358 */ 2359 protected static function compute_theme_vars( $settings ) { 2360 $declarations = array(); 2361 $custom_values = $settings['custom'] ?? array(); 2362 $css_vars = static::flatten_tree( $custom_values ); 2363 foreach ( $css_vars as $key => $value ) { 2364 $declarations[] = array( 2365 'name' => '--wp--custom--' . $key, 2366 'value' => $value, 2367 ); 2368 } 2369 2370 return $declarations; 2371 } 2372 2373 /** 2374 * Given a tree, it creates a flattened one 2375 * by merging the keys and binding the leaf values 2376 * to the new keys. 2377 * 2378 * It also transforms camelCase names into kebab-case 2379 * and substitutes '/' by '-'. 2380 * 2381 * This is thought to be useful to generate 2382 * CSS Custom Properties from a tree, 2383 * although there's nothing in the implementation 2384 * of this function that requires that format. 2385 * 2386 * For example, assuming the given prefix is '--wp' 2387 * and the token is '--', for this input tree: 2388 * 2389 * { 2390 * 'some/property': 'value', 2391 * 'nestedProperty': { 2392 * 'sub-property': 'value' 2393 * } 2394 * } 2395 * 2396 * it'll return this output: 2397 * 2398 * { 2399 * '--wp--some-property': 'value', 2400 * '--wp--nested-property--sub-property': 'value' 2401 * } 2402 * 2403 * @since 5.8.0 2404 * 2405 * @param array $tree Input tree to process. 2406 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. 2407 * @param string $token Optional. Token to use between levels. Default '--'. 2408 * @return array The flattened tree. 2409 */ 2410 protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { 2411 $result = array(); 2412 foreach ( $tree as $property => $value ) { 2413 $new_key = $prefix . str_replace( 2414 '/', 2415 '-', 2416 strtolower( _wp_to_kebab_case( $property ) ) 2417 ); 2418 2419 if ( is_array( $value ) ) { 2420 $new_prefix = $new_key . $token; 2421 $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token ); 2422 foreach ( $flattened_subtree as $subtree_key => $subtree_value ) { 2423 $result[ $subtree_key ] = $subtree_value; 2424 } 2425 } else { 2426 $result[ $new_key ] = $value; 2427 } 2428 } 2429 return $result; 2430 } 2431 2432 /** 2433 * Given a styles array, it extracts the style properties 2434 * and adds them to the $declarations array following the format: 2435 * 2436 * array( 2437 * 'name' => 'property_name', 2438 * 'value' => 'property_value', 2439 * ) 2440 * 2441 * @since 5.8.0 2442 * @since 5.9.0 Added the `$settings` and `$properties` parameters. 2443 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters. 2444 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set. 2445 * @since 6.6.0 Pass current theme JSON settings to wp_get_typography_font_size_value(), and process background properties. 2446 * @since 6.7.0 `ref` resolution of background properties, and assigning custom default values. 2447 * 2448 * @param array $styles Styles to process. 2449 * @param array $settings Theme settings. 2450 * @param array $properties Properties metadata. 2451 * @param array $theme_json Theme JSON array. 2452 * @param string $selector The style block selector. 2453 * @param boolean $use_root_padding Whether to add custom properties at root level. 2454 * @return array Returns the modified $declarations. 2455 */ 2456 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { 2457 if ( empty( $styles ) ) { 2458 return array(); 2459 } 2460 2461 if ( null === $properties ) { 2462 $properties = static::PROPERTIES_METADATA; 2463 } 2464 $declarations = array(); 2465 $root_variable_duplicates = array(); 2466 $root_style_length = strlen( '--wp--style--root--' ); 2467 2468 foreach ( $properties as $css_property => $value_path ) { 2469 if ( ! is_array( $value_path ) ) { 2470 continue; 2471 } 2472 2473 $is_root_style = str_starts_with( $css_property, '--wp--style--root--' ); 2474 if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { 2475 continue; 2476 } 2477 2478 $value = static::get_property_value( $styles, $value_path, $theme_json ); 2479 2480 /* 2481 * Root-level padding styles don't currently support strings with CSS shorthand values. 2482 * This may change: https://github.com/WordPress/gutenberg/issues/40132. 2483 */ 2484 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { 2485 continue; 2486 } 2487 2488 if ( $is_root_style && $use_root_padding ) { 2489 $root_variable_duplicates[] = substr( $css_property, $root_style_length ); 2490 } 2491 2492 /* 2493 * Processes background image styles. 2494 * If the value is a URL, it will be converted to a CSS `url()` value. 2495 * For uploaded image (images with a database ID), apply size and position defaults, 2496 * equal to those applied in block supports in lib/background.php. 2497 */ 2498 if ( 'background-image' === $css_property && ! empty( $value ) ) { 2499 $background_styles = wp_style_engine_get_styles( 2500 array( 'background' => array( 'backgroundImage' => $value ) ) 2501 ); 2502 $value = $background_styles['declarations'][ $css_property ]; 2503 } 2504 if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) { 2505 if ( 'background-size' === $css_property ) { 2506 $value = 'cover'; 2507 } 2508 // If the background size is set to `contain` and no position is set, set the position to `center`. 2509 if ( 'background-position' === $css_property ) { 2510 $background_size = $styles['background']['backgroundSize'] ?? null; 2511 $value = 'contain' === $background_size ? '50% 50%' : null; 2512 } 2513 } 2514 2515 // Skip if empty and not "0" or value represents array of longhand values. 2516 $has_missing_value = empty( $value ) && ! is_numeric( $value ); 2517 if ( $has_missing_value || is_array( $value ) ) { 2518 continue; 2519 } 2520 2521 // Calculates fluid typography rules where available. 2522 if ( 'font-size' === $css_property ) { 2523 /* 2524 * wp_get_typography_font_size_value() will check 2525 * if fluid typography has been activated and also 2526 * whether the incoming value can be converted to a fluid value. 2527 * Values that already have a clamp() function will not pass the test, 2528 * and therefore the original $value will be returned. 2529 * Pass the current theme_json settings to override any global settings. 2530 */ 2531 $value = wp_get_typography_font_size_value( array( 'size' => $value ), $settings ); 2532 } 2533 2534 if ( 'aspect-ratio' === $css_property ) { 2535 // For aspect ratio to work, other dimensions rules must be unset. 2536 // This ensures that a fixed height does not override the aspect ratio. 2537 $declarations[] = array( 2538 'name' => 'min-height', 2539 'value' => 'unset', 2540 ); 2541 } 2542 2543 $declarations[] = array( 2544 'name' => $css_property, 2545 'value' => $value, 2546 ); 2547 } 2548 2549 // If a variable value is added to the root, the corresponding property should be removed. 2550 foreach ( $root_variable_duplicates as $duplicate ) { 2551 $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); 2552 if ( is_numeric( $discard ) ) { 2553 array_splice( $declarations, $discard, 1 ); 2554 } 2555 } 2556 2557 return $declarations; 2558 } 2559 2560 /** 2561 * Returns the style property for the given path. 2562 * 2563 * It also converts references to a path to the value 2564 * stored at that location, e.g. 2565 * { "ref": "style.color.background" } => "#fff". 2566 * 2567 * @since 5.8.0 2568 * @since 5.9.0 Added support for values of array type, which are returned as is. 2569 * @since 6.1.0 Added the `$theme_json` parameter. 2570 * @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary" 2571 * to the standard form "--wp--preset--color--secondary". 2572 * This is already done by the sanitize method, 2573 * so every property will be in the standard form. 2574 * @since 6.7.0 Added support for background image refs. 2575 * 2576 * @param array $styles Styles subtree. 2577 * @param array $path Which property to process. 2578 * @param array $theme_json Theme JSON array. 2579 * @return string|array Style property value. 2580 */ 2581 protected static function get_property_value( $styles, $path, $theme_json = null ) { 2582 $value = _wp_array_get( $styles, $path, '' ); 2583 2584 if ( '' === $value || null === $value ) { 2585 // No need to process the value further. 2586 return ''; 2587 } 2588 2589 /* 2590 * This converts references to a path to the value at that path 2591 * where the value is an array with a "ref" key, pointing to a path. 2592 * For example: { "ref": "style.color.background" } => "#fff". 2593 * In the case of backgroundImage, if both a ref and a URL are present in the value, 2594 * the URL takes precedence and the ref is ignored. 2595 */ 2596 if ( is_array( $value ) && isset( $value['ref'] ) ) { 2597 $value_path = explode( '.', $value['ref'] ); 2598 $ref_value = _wp_array_get( $theme_json, $value_path ); 2599 // Background Image refs can refer to a string or an array containing a URL string. 2600 $ref_value_url = $ref_value['url'] ?? null; 2601 // Only use the ref value if we find anything. 2602 if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) { 2603 $value = $ref_value; 2604 } 2605 2606 if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) { 2607 $path_string = json_encode( $path ); 2608 $ref_value_string = json_encode( $ref_value ); 2609 _doing_it_wrong( 2610 'get_property_value', 2611 sprintf( 2612 /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ 2613 __( '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.' ), 2614 'theme.json', 2615 $ref_value_string, 2616 $path_string, 2617 $ref_value['ref'] 2618 ), 2619 '6.1.0' 2620 ); 2621 } 2622 } 2623 2624 return $value; 2625 } 2626 2627 /** 2628 * Builds metadata for the setting nodes, which returns in the form of: 2629 * 2630 * [ 2631 * [ 2632 * 'path' => ['path', 'to', 'some', 'node' ], 2633 * 'selector' => 'CSS selector for some node' 2634 * ], 2635 * [ 2636 * 'path' => [ 'path', 'to', 'other', 'node' ], 2637 * 'selector' => 'CSS selector for other node' 2638 * ], 2639 * ] 2640 * 2641 * @since 5.8.0 2642 * 2643 * @param array $theme_json The tree to extract setting nodes from. 2644 * @param array $selectors List of selectors per block. 2645 * @return array An array of setting nodes metadata. 2646 */ 2647 protected static function get_setting_nodes( $theme_json, $selectors = array() ) { 2648 $nodes = array(); 2649 if ( ! isset( $theme_json['settings'] ) ) { 2650 return $nodes; 2651 } 2652 2653 // Top-level. 2654 $nodes[] = array( 2655 'path' => array( 'settings' ), 2656 'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR, 2657 ); 2658 2659 // Calculate paths for blocks. 2660 if ( ! isset( $theme_json['settings']['blocks'] ) ) { 2661 return $nodes; 2662 } 2663 2664 foreach ( $theme_json['settings']['blocks'] as $name => $node ) { 2665 $selector = null; 2666 if ( isset( $selectors[ $name ]['selector'] ) ) { 2667 $selector = $selectors[ $name ]['selector']; 2668 } 2669 2670 $nodes[] = array( 2671 'path' => array( 'settings', 'blocks', $name ), 2672 'selector' => $selector, 2673 ); 2674 } 2675 2676 return $nodes; 2677 } 2678 2679 /** 2680 * Builds metadata for the style nodes, which returns in the form of: 2681 * 2682 * [ 2683 * [ 2684 * 'path' => [ 'path', 'to', 'some', 'node' ], 2685 * 'selector' => 'CSS selector for some node', 2686 * 'duotone' => 'CSS selector for duotone for some node' 2687 * ], 2688 * [ 2689 * 'path' => ['path', 'to', 'other', 'node' ], 2690 * 'selector' => 'CSS selector for other node', 2691 * 'duotone' => null 2692 * ], 2693 * ] 2694 * 2695 * @since 5.8.0 2696 * @since 6.6.0 Added options array for modifying generated nodes. 2697 * 2698 * @param array $theme_json The tree to extract style nodes from. 2699 * @param array $selectors List of selectors per block. 2700 * @param array $options { 2701 * Optional. An array of options for now used for internal purposes only (may change without notice). 2702 * 2703 * @type bool $include_block_style_variations Includes style nodes for block style variations. Default false. 2704 * } 2705 * @return array An array of style nodes metadata. 2706 */ 2707 protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) { 2708 $nodes = array(); 2709 if ( ! isset( $theme_json['styles'] ) ) { 2710 return $nodes; 2711 } 2712 2713 // Top-level. 2714 $nodes[] = array( 2715 'path' => array( 'styles' ), 2716 'selector' => static::ROOT_BLOCK_SELECTOR, 2717 ); 2718 2719 if ( isset( $theme_json['styles']['elements'] ) ) { 2720 foreach ( self::ELEMENTS as $element => $selector ) { 2721 if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) { 2722 continue; 2723 } 2724 $nodes[] = array( 2725 'path' => array( 'styles', 'elements', $element ), 2726 'selector' => static::ELEMENTS[ $element ], 2727 ); 2728 2729 // Handle any pseudo selectors for the element. 2730 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { 2731 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 2732 2733 if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { 2734 $nodes[] = array( 2735 'path' => array( 'styles', 'elements', $element ), 2736 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), 2737 ); 2738 } 2739 } 2740 } 2741 } 2742 } 2743 2744 // Blocks. 2745 if ( ! isset( $theme_json['styles']['blocks'] ) ) { 2746 return $nodes; 2747 } 2748 2749 $block_nodes = static::get_block_nodes( $theme_json, $selectors, $options ); 2750 foreach ( $block_nodes as $block_node ) { 2751 $nodes[] = $block_node; 2752 } 2753 2754 /** 2755 * Filters the list of style nodes with metadata. 2756 * 2757 * This allows for things like loading block CSS independently. 2758 * 2759 * @since 6.1.0 2760 * 2761 * @param array $nodes Style nodes with metadata. 2762 */ 2763 return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); 2764 } 2765 2766 /** 2767 * A public helper to get the block nodes from a theme.json file. 2768 * 2769 * @since 6.1.0 2770 * 2771 * @return array The block nodes in theme.json. 2772 */ 2773 public function get_styles_block_nodes() { 2774 return static::get_block_nodes( $this->theme_json ); 2775 } 2776 2777 /** 2778 * Returns a filtered declarations array if there is a separator block with only a background 2779 * style defined in theme.json by adding a color attribute to reflect the changes in the front. 2780 * 2781 * @since 6.1.1 2782 * 2783 * @param array $declarations List of declarations. 2784 * @return array $declarations List of declarations filtered. 2785 */ 2786 private static function update_separator_declarations( $declarations ) { 2787 $background_color = ''; 2788 $border_color_matches = false; 2789 $text_color_matches = false; 2790 2791 foreach ( $declarations as $declaration ) { 2792 if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) { 2793 $background_color = $declaration['value']; 2794 } elseif ( 'border-color' === $declaration['name'] ) { 2795 $border_color_matches = true; 2796 } elseif ( 'color' === $declaration['name'] ) { 2797 $text_color_matches = true; 2798 } 2799 2800 if ( $background_color && $border_color_matches && $text_color_matches ) { 2801 break; 2802 } 2803 } 2804 2805 if ( $background_color && ! $border_color_matches && ! $text_color_matches ) { 2806 $declarations[] = array( 2807 'name' => 'color', 2808 'value' => $background_color, 2809 ); 2810 } 2811 2812 return $declarations; 2813 } 2814 2815 /** 2816 * Updates the text indent selector for paragraph blocks based on the textIndent setting. 2817 * 2818 * The textIndent setting can be 'subsequent' (default), 'all', or false. 2819 * When set to 'all', the selector should be '.wp-block-paragraph' instead of 2820 * '.wp-block-paragraph + .wp-block-paragraph' to apply indent to all paragraphs. 2821 * 2822 * @since 7.0.0 2823 * 2824 * @param array $feature_declarations The feature declarations keyed by selector. 2825 * @param array $settings The theme.json settings. 2826 * @param string $block_name The block name being processed. 2827 * @return array The updated feature declarations. 2828 */ 2829 private static function update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ) { 2830 if ( 'core/paragraph' !== $block_name ) { 2831 return $feature_declarations; 2832 } 2833 2834 // Check block-level settings first, then fall back to global settings. 2835 $block_settings = $settings['blocks']['core/paragraph'] ?? null; 2836 $text_indent_setting = $block_settings['typography']['textIndent'] 2837 ?? $settings['typography']['textIndent'] 2838 ?? 'subsequent'; 2839 2840 if ( 'all' !== $text_indent_setting ) { 2841 return $feature_declarations; 2842 } 2843 2844 // Look for the text indent selector and replace it. 2845 $old_selector = '.wp-block-paragraph + .wp-block-paragraph'; 2846 $new_selector = '.wp-block-paragraph'; 2847 2848 if ( isset( $feature_declarations[ $old_selector ] ) ) { 2849 $declarations = $feature_declarations[ $old_selector ]; 2850 unset( $feature_declarations[ $old_selector ] ); 2851 $feature_declarations[ $new_selector ] = $declarations; 2852 } 2853 2854 return $feature_declarations; 2855 } 2856 2857 /** 2858 * An internal method to get the block nodes from a theme.json file. 2859 * 2860 * @since 6.1.0 2861 * @since 6.3.0 Refactored and stabilized selectors API. 2862 * @since 6.6.0 Added optional selectors and options for generating block nodes. 2863 * @since 6.7.0 Added $include_node_paths_only option. 2864 * @since 7.1.0 Added responsive block nodes for breakpoint-based styles. 2865 * 2866 * @param array $theme_json The theme.json converted to an array. 2867 * @param array $selectors Optional list of selectors per block. 2868 * @param array $options { 2869 * Optional. An array of options for now used for internal purposes only (may change without notice). 2870 * 2871 * @type bool $include_block_style_variations Include nodes for block style variations. Default false. 2872 * @type bool $include_node_paths_only Return only block nodes node paths. Default false. 2873 * } 2874 * @return array The block nodes in theme.json. 2875 */ 2876 private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) { 2877 $nodes = array(); 2878 2879 if ( ! isset( $theme_json['styles']['blocks'] ) ) { 2880 return $nodes; 2881 } 2882 2883 $include_variations = $options['include_block_style_variations'] ?? false; 2884 $include_node_paths_only = $options['include_node_paths_only'] ?? false; 2885 2886 // If only node paths are to be returned, skip selector assignment. 2887 if ( ! $include_node_paths_only ) { 2888 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; 2889 } 2890 2891 foreach ( $theme_json['styles']['blocks'] as $name => $node ) { 2892 $node_path = array( 'styles', 'blocks', $name ); 2893 if ( $include_node_paths_only ) { 2894 $variation_paths = array(); 2895 if ( $include_variations && isset( $node['variations'] ) ) { 2896 foreach ( $node['variations'] as $variation => $variation_node ) { 2897 $variation_paths[] = array( 2898 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), 2899 ); 2900 } 2901 } 2902 $node = array( 2903 'path' => $node_path, 2904 ); 2905 if ( ! empty( $variation_paths ) ) { 2906 $node['variations'] = $variation_paths; 2907 } 2908 $nodes[] = $node; 2909 } else { 2910 $selector = null; 2911 if ( isset( $selectors[ $name ]['selector'] ) ) { 2912 $selector = $selectors[ $name ]['selector']; 2913 } 2914 2915 $duotone_selector = null; 2916 if ( isset( $selectors[ $name ]['duotone'] ) ) { 2917 $duotone_selector = $selectors[ $name ]['duotone']; 2918 } 2919 2920 $feature_selectors = null; 2921 if ( isset( $selectors[ $name ]['selectors'] ) ) { 2922 $feature_selectors = $selectors[ $name ]['selectors']; 2923 } 2924 2925 $variation_selectors = array(); 2926 2927 if ( $include_variations && isset( $node['variations'] ) ) { 2928 foreach ( $node['variations'] as $variation => $node ) { 2929 $variation_selectors[] = array( 2930 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), 2931 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], 2932 ); 2933 } 2934 } 2935 2936 $nodes[] = array( 2937 'name' => $name, 2938 'path' => $node_path, 2939 'selector' => $selector, 2940 'selectors' => $feature_selectors, 2941 'elements' => $selectors[ $name ]['elements'] ?? array(), 2942 'duotone' => $duotone_selector, 2943 'variations' => $variation_selectors, 2944 'css' => $selector, 2945 ); 2946 2947 // Responsive block nodes: emit one node per breakpoint that has styles. 2948 // These are rendered immediately after the base block node so that 2949 // the cascade order is: .block{} → @media{.block{}} 2950 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 2951 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) { 2952 $nodes[] = array( 2953 'name' => $name, 2954 'path' => array( 'styles', 'blocks', $name, $breakpoint ), 2955 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 2956 'selector' => $selector, 2957 'selectors' => $feature_selectors, 2958 'elements' => $selectors[ $name ]['elements'] ?? array(), 2959 'variations' => $variation_selectors, 2960 'css' => $selector, 2961 ); 2962 } 2963 } 2964 2965 // Handle any pseudo selectors for the block. 2966 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) { 2967 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) { 2968 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ); 2969 $has_responsive_pseudo = false; 2970 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 2971 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) { 2972 $has_responsive_pseudo = true; 2973 break; 2974 } 2975 } 2976 2977 if ( ! $has_pseudo && ! $has_responsive_pseudo ) { 2978 continue; 2979 } 2980 2981 /* 2982 * Append the pseudo-selector to each feature selector so that 2983 * get_feature_declarations_for_node generates CSS scoped to the 2984 * pseudo-state (e.g. '.wp-block-button:hover') rather than the 2985 * default state (e.g. '.wp-block-button'). 2986 */ 2987 $pseudo_feature_selectors = array(); 2988 foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) { 2989 if ( is_array( $feature_selector ) ) { 2990 $pseudo_feature_selectors[ $feature ] = array(); 2991 foreach ( $feature_selector as $subfeature => $subfeature_selector ) { 2992 $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector ); 2993 } 2994 } else { 2995 $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector ); 2996 } 2997 } 2998 2999 if ( $has_pseudo ) { 3000 $nodes[] = array( 3001 'name' => $name, 3002 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ), 3003 'selector' => static::append_to_selector( $selector, $pseudo_selector ), 3004 'selectors' => $pseudo_feature_selectors, 3005 'elements' => $selectors[ $name ]['elements'] ?? array(), 3006 'duotone' => $duotone_selector, 3007 'variations' => $variation_selectors, 3008 'css' => static::append_to_selector( $selector, $pseudo_selector ), 3009 ); 3010 } 3011 3012 // Responsive pseudo nodes: emit one node per breakpoint that has 3013 // this pseudo state, immediately after the default pseudo node. 3014 // Cascade order: .block:hover{} → @media{.block:hover{}} 3015 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3016 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) { 3017 $nodes[] = array( 3018 'name' => $name, 3019 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ), 3020 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3021 'selector' => static::append_to_selector( $selector, $pseudo_selector ), 3022 'selectors' => $pseudo_feature_selectors, 3023 'elements' => $selectors[ $name ]['elements'] ?? array(), 3024 'variations' => $variation_selectors, 3025 'css' => static::append_to_selector( $selector, $pseudo_selector ), 3026 ); 3027 } 3028 } 3029 } 3030 } 3031 } 3032 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { 3033 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { 3034 $element_path = array( 'styles', 'blocks', $name, 'elements', $element ); 3035 if ( $include_node_paths_only ) { 3036 $nodes[] = array( 3037 'path' => $element_path, 3038 ); 3039 continue; 3040 } 3041 3042 $element_selector = $selectors[ $name ]['elements'][ $element ]; 3043 3044 $nodes[] = array( 3045 'path' => $element_path, 3046 'selector' => $element_selector, 3047 ); 3048 3049 // Responsive element nodes: one node per breakpoint that has 3050 // styles for this element. Cascade: a{} → @media{a{}} 3051 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3052 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) { 3053 $nodes[] = array( 3054 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), 3055 'selector' => $element_selector, 3056 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3057 ); 3058 } 3059 } 3060 3061 // Handle any pseudo selectors for the element. 3062 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { 3063 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 3064 // Create element pseudo node if default or any responsive breakpoint has the pseudo. 3065 $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ); 3066 if ( ! $has_element_pseudo ) { 3067 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) { 3068 if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) { 3069 $has_element_pseudo = true; 3070 break; 3071 } 3072 } 3073 } 3074 3075 if ( $has_element_pseudo ) { 3076 $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element ); 3077 if ( $include_node_paths_only ) { 3078 $nodes[] = array( 3079 'path' => $element_pseudo_path, 3080 ); 3081 continue; 3082 } 3083 3084 $nodes[] = array( 3085 'path' => $element_pseudo_path, 3086 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), 3087 ); 3088 3089 // Responsive element pseudo nodes: one node per breakpoint 3090 // that has this pseudo state for this element. 3091 // Cascade: a:hover{} → @media{a:hover{}} 3092 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3093 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) { 3094 $nodes[] = array( 3095 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), 3096 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), 3097 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3098 ); 3099 } 3100 } 3101 } 3102 } 3103 } 3104 } 3105 } 3106 } 3107 3108 return $nodes; 3109 } 3110 3111 /** 3112 * Gets the CSS rules for a particular block from theme.json. 3113 * 3114 * @since 6.1.0 3115 * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image. 3116 * Updated general global styles specificity to 0-1-0. 3117 * Fixed custom CSS output in block style variations. 3118 * 3119 * @param array $block_metadata Metadata about the block to get styles for. 3120 * @return string Styles for the block. 3121 */ 3122 public function get_styles_for_block( $block_metadata ) { 3123 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); 3124 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; 3125 $selector = $block_metadata['selector']; 3126 $settings = $this->theme_json['settings'] ?? array(); 3127 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node ); 3128 $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector; 3129 $media_query = $block_metadata['media_query'] ?? null; 3130 3131 // Update text indent selector for paragraph blocks based on the textIndent setting. 3132 $block_name = $block_metadata['name'] ?? null; 3133 $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ); 3134 $block_elements = $block_metadata['elements'] ?? array(); 3135 3136 // If there are style variations, generate the declarations for them, including any feature selectors the block may have. 3137 $style_variation_declarations = array(); 3138 $style_variation_custom_css = array(); 3139 $style_variation_responsive_css = array(); 3140 $style_variation_layout_metadata = array(); 3141 if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) { 3142 foreach ( $block_metadata['variations'] as $style_variation ) { 3143 $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); 3144 $clean_style_variation_selector = trim( $style_variation['selector'] ); 3145 3146 // Generate any feature/subfeature style declarations for the current style variation. 3147 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node ); 3148 3149 // Update text indent selector for paragraph blocks based on the textIndent setting. 3150 $variation_declarations = static::update_paragraph_text_indent_selector( $variation_declarations, $settings, $block_name ); 3151 3152 // Combine selectors with style variation's selector and add to overall style variation declarations. 3153 foreach ( $variation_declarations as $current_selector => $new_declarations ) { 3154 /* 3155 * Clean up any whitespace between comma separated selectors. 3156 * This prevents these spaces breaking compound selectors such as: 3157 * - `.wp-block-list:not(.wp-block-list .wp-block-list)` 3158 * - `.wp-block-image img, .wp-block-image.my-class img` 3159 */ 3160 $clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector ); 3161 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector ); 3162 3163 // Prepend the variation selector to the current selector. 3164 $split_selectors = explode( ',', $shortened_selector ); 3165 $updated_selectors = array_map( 3166 static function ( $split_selector ) use ( $clean_style_variation_selector ) { 3167 return $clean_style_variation_selector . $split_selector; 3168 }, 3169 $split_selectors 3170 ); 3171 $combined_selectors = implode( ',', $updated_selectors ); 3172 3173 // Add the new declarations to the overall results under the modified selector. 3174 $style_variation_declarations[ $combined_selectors ] = $new_declarations; 3175 } 3176 3177 // Compute declarations for remaining styles not covered by feature level selectors. 3178 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); 3179 3180 // Process pseudo-selectors for this variation (e.g., :hover, :focus) 3181 if ( isset( $block_metadata['name'] ) ) { 3182 $block_name = $block_metadata['name']; 3183 } elseif ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ) { 3184 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); 3185 } else { 3186 $block_name = null; 3187 } 3188 $variation_pseudo_declarations = static::process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name ); 3189 $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations ); 3190 3191 // Store custom CSS for the style variation. 3192 if ( isset( $style_variation_node['css'] ) ) { 3193 $style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] ); 3194 } 3195 3196 // Store variation metadata and node for layout styles generation. 3197 // Only store if the variation has blockGap defined. 3198 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) { 3199 // Append block selector to the variation selector for proper targeting. 3200 $variation_metadata_with_selector = $style_variation; 3201 $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css']; 3202 $style_variation_layout_metadata[ $style_variation['selector'] ] = array( 3203 'metadata' => $variation_metadata_with_selector, 3204 'node' => $style_variation_node, 3205 ); 3206 } 3207 3208 // Store responsive breakpoint CSS for the style variation. 3209 // This includes both base properties and feature-level selectors. 3210 $variation_responsive_css = ''; 3211 3212 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3213 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) { 3214 continue; 3215 } 3216 3217 $breakpoint_node = $style_variation_node[ $breakpoint ]; 3218 $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ]; 3219 // Process feature-level declarations for this breakpoint. 3220 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node ); 3221 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name ); 3222 foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) { 3223 $clean_feature_selector = preg_replace( '/,\s+/', ',', $feature_selector ); 3224 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_feature_selector ); 3225 3226 if ( $block_metadata['selector'] && ! str_contains( $clean_feature_selector, $block_metadata['selector'] ) ) { 3227 /* 3228 * Feature selector is block-level (e.g. `.wp-block-button` for 3229 * dimensions/width) — apply the variation class directly to it. 3230 */ 3231 $feature_element_selector = str_replace( $shortened_selector, '', $clean_style_variation_selector ); 3232 $combined_selectors = str_replace( $feature_element_selector, '', $clean_style_variation_selector ); 3233 } else { 3234 // Prepend the variation selector to the current selector. 3235 $split_selectors = explode( ',', $shortened_selector ); 3236 $updated_selectors = array_map( 3237 static function ( $split_selector ) use ( $clean_style_variation_selector ) { 3238 return $clean_style_variation_selector . $split_selector; 3239 }, 3240 $split_selectors 3241 ); 3242 $combined_selectors = implode( ',', $updated_selectors ); 3243 } 3244 3245 $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl ); 3246 $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}'; 3247 } 3248 3249 // Process base properties for this breakpoint. 3250 $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json ); 3251 if ( ! empty( $breakpoint_declarations ) ) { 3252 $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations ); 3253 $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}'; 3254 } 3255 3256 $breakpoint_pseudo_declarations = static::process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name ); 3257 foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) { 3258 if ( empty( $pseudo_declarations ) ) { 3259 continue; 3260 } 3261 $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations ); 3262 $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}'; 3263 } 3264 3265 // Process custom CSS for this breakpoint. 3266 if ( isset( $breakpoint_node['css'] ) ) { 3267 $breakpoint_custom_css = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] ); 3268 $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}'; 3269 } 3270 3271 // Process blockGap responsive layout styles for this variation. 3272 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) { 3273 $variation_layout_metadata = $style_variation; 3274 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css']; 3275 $variation_responsive_css .= $this->get_layout_styles( 3276 $variation_layout_metadata, 3277 array( 3278 'node' => $breakpoint_node, 3279 'media_query' => $breakpoint_media, 3280 ) 3281 ); 3282 } 3283 3284 // Process nested element styles for this breakpoint state. 3285 if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) { 3286 foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) { 3287 if ( ! isset( $block_elements[ $element_name ] ) ) { 3288 continue; 3289 } 3290 3291 $clean_element_selector = preg_replace( '/,\s+/', ',', $block_elements[ $element_name ] ); 3292 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_element_selector ); 3293 $split_selectors = explode( ',', $shortened_selector ); 3294 $updated_selectors = array_map( 3295 static function ( $split_selector ) use ( $clean_style_variation_selector ) { 3296 return $clean_style_variation_selector . $split_selector; 3297 }, 3298 $split_selectors 3299 ); 3300 $variation_element_selector = implode( ',', $updated_selectors ); 3301 3302 $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json ); 3303 if ( ! empty( $element_declarations ) ) { 3304 $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations ); 3305 $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}'; 3306 } 3307 3308 if ( isset( $element_node['css'] ) ) { 3309 $element_custom_css = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector ); 3310 $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}'; 3311 } 3312 3313 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { 3314 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { 3315 if ( ! isset( $element_node[ $pseudo_selector ] ) ) { 3316 continue; 3317 } 3318 3319 $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json ); 3320 if ( empty( $pseudo_declarations ) ) { 3321 continue; 3322 } 3323 3324 $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations ); 3325 $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}'; 3326 } 3327 } 3328 } 3329 } 3330 } 3331 3332 if ( ! empty( $variation_responsive_css ) ) { 3333 $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css; 3334 } 3335 } 3336 } 3337 /* 3338 * Get a reference to element name from path. 3339 * $block_metadata['path'] = array( 'styles','elements','link' ); 3340 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. 3341 * Skip non-element paths like just ['styles']. 3342 */ 3343 $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); 3344 3345 $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; 3346 3347 $element_pseudo_allowed = array(); 3348 3349 if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { 3350 $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; 3351 } 3352 3353 /* 3354 * Check if we're processing a block pseudo-selector. 3355 * $block_metadata['path'] = array( 'styles', 'blocks', 'core/button', ':hover' ); 3356 */ 3357 $is_processing_block_pseudo = false; 3358 $block_pseudo_selector = null; 3359 if ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 4 ) { 3360 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); // 'core/button' 3361 $last_path_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; // ':hover' 3362 3363 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) && 3364 in_array( $last_path_element, static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ], true ) ) { 3365 $is_processing_block_pseudo = true; 3366 $block_pseudo_selector = $last_path_element; 3367 } 3368 } 3369 3370 /* 3371 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). 3372 * This also resets the array keys. 3373 */ 3374 $pseudo_matches = array_values( 3375 array_filter( 3376 $element_pseudo_allowed, 3377 static function ( $pseudo_selector ) use ( $selector ) { 3378 /* 3379 * Check if the pseudo selector is in the current selector, 3380 * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible). 3381 */ 3382 return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1; 3383 } 3384 ) 3385 ); 3386 3387 $pseudo_selector = $pseudo_matches[0] ?? null; 3388 3389 /* 3390 * If the current selector is a pseudo selector that's defined in the allow list for the current 3391 * element then compute the style properties for it. 3392 * Otherwise just compute the styles for the default selector as normal. 3393 */ 3394 if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && 3395 isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) 3396 && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) 3397 ) { 3398 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); 3399 } elseif ( $is_processing_block_pseudo ) { 3400 // Process block pseudo-selector styles 3401 // For block pseudo-selectors, we need to get the block data first, then access the pseudo-selector 3402 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); // 'core/button' 3403 $block_data = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $block_name ), array() ); 3404 $pseudo_data = $block_data[ $block_pseudo_selector ] ?? array(); 3405 3406 $declarations = static::compute_style_properties( $pseudo_data, $settings, null, $this->theme_json, $selector, $use_root_padding ); 3407 } else { 3408 $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); 3409 } 3410 3411 $block_rules = ''; 3412 3413 /* 3414 * 1. Bespoke declaration modifiers: 3415 * - 'filter': Separate the declarations that use the general selector 3416 * from the ones using the duotone selector. 3417 * - 'background|background-image': set the html min-height to 100% 3418 * to ensure the background covers the entire viewport. 3419 */ 3420 $declarations_duotone = array(); 3421 $should_set_root_min_height = false; 3422 3423 foreach ( $declarations as $index => $declaration ) { 3424 if ( 'filter' === $declaration['name'] ) { 3425 /* 3426 * 'unset' filters happen when a filter is unset 3427 * in the site-editor UI. Because the 'unset' value 3428 * in the user origin overrides the value in the 3429 * theme origin, we can skip rendering anything 3430 * here as no filter needs to be applied anymore. 3431 * So only add declarations to with values other 3432 * than 'unset'. 3433 */ 3434 if ( 'unset' !== $declaration['value'] ) { 3435 $declarations_duotone[] = $declaration; 3436 } 3437 unset( $declarations[ $index ] ); 3438 } 3439 3440 if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) { 3441 $should_set_root_min_height = true; 3442 } 3443 } 3444 3445 /* 3446 * If root styles has a background-image or a background (gradient) set, 3447 * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view. 3448 * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly, 3449 * and matches the behavior of the site editor. 3450 */ 3451 if ( $should_set_root_min_height ) { 3452 $block_rules .= static::to_ruleset( 3453 'html', 3454 array( 3455 array( 3456 'name' => 'min-height', 3457 'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))', 3458 ), 3459 ) 3460 ); 3461 } 3462 3463 // Update declarations if there are separators with only background color defined. 3464 if ( '.wp-block-separator' === $selector ) { 3465 $declarations = static::update_separator_declarations( $declarations ); 3466 } 3467 3468 /* 3469 * Root selector (body) styles should not be wrapped in `:root where()` to keep 3470 * specificity at (0,0,1) and maintain backwards compatibility. 3471 * 3472 * Top-level element styles using element-only specificity selectors should 3473 * not get wrapped in `:root :where()` to maintain backwards compatibility. 3474 * 3475 * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so 3476 * still need to be wrapped in `:root :where` to cap specificity for nested 3477 * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly. 3478 */ 3479 $element_only_selector = $is_root_selector || ( 3480 $current_element && 3481 isset( static::ELEMENTS[ $current_element ] ) && 3482 // buttons, captions etc. still need `:root :where()` as they are class based selectors. 3483 ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) && 3484 static::ELEMENTS[ $current_element ] === $selector 3485 ); 3486 3487 // 2. Generate and append the rules that use the general selector. 3488 $general_selector = $element_only_selector ? $selector : ":root :where($selector)"; 3489 $block_rules .= static::to_ruleset( $general_selector, $declarations ); 3490 3491 // 3. Generate and append the rules that use the duotone selector. 3492 if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { 3493 $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone ); 3494 } 3495 3496 // 4. Generate Layout block gap styles. 3497 if ( 3498 ! $is_root_selector && 3499 ! empty( $block_metadata['name'] ) 3500 ) { 3501 $block_rules .= $this->get_layout_styles( $block_metadata ); 3502 } 3503 3504 // 5. Generate and append the feature level rulesets. 3505 foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { 3506 $block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations ); 3507 } 3508 3509 // 6. Generate and append the style variation rulesets. 3510 foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) { 3511 $block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations ); 3512 if ( isset( $style_variation_layout_metadata[ $style_variation_selector ] ) ) { 3513 $variation_data = $style_variation_layout_metadata[ $style_variation_selector ]; 3514 $block_rules .= $this->get_layout_styles( $variation_data['metadata'], array( 'node' => $variation_data['node'] ) ); 3515 } 3516 if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) { 3517 $block_rules .= $style_variation_custom_css[ $style_variation_selector ]; 3518 } 3519 if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) { 3520 $block_rules .= $style_variation_responsive_css[ $style_variation_selector ]; 3521 } 3522 } 3523 3524 // 7. Generate and append any custom CSS rules. 3525 if ( isset( $node['css'] ) && ! $is_root_selector ) { 3526 $block_rules .= $this->process_blocks_custom_css( $node['css'], $selector ); 3527 } 3528 3529 // 8. Wrap the entire block output in a media query if this is a responsive node. 3530 // Responsive nodes are created by get_block_nodes() for each breakpoint and carry 3531 // a 'media_query' key. 3532 if ( $media_query && ! empty( $block_rules ) ) { 3533 $block_rules = $media_query . '{' . $block_rules . '}'; 3534 } 3535 3536 return $block_rules; 3537 } 3538 3539 /** 3540 * Outputs the CSS for layout rules on the root. 3541 * 3542 * @since 6.1.0 3543 * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties and improved consistency of root padding rules. 3544 * Updated specificity of body margin reset and first/last child selectors. 3545 * @since 7.0.0 Added `$options` parameter to control alignment styles output for classic themes. 3546 * 3547 * @param string $selector The root node selector. 3548 * @param array $block_metadata The metadata for the root block. 3549 * @param array $options Optional. An array of options for now used for internal purposes only. 3550 * @return string The additional root rules CSS. 3551 */ 3552 public function get_root_layout_rules( $selector, $block_metadata, $options = array() ) { 3553 $css = ''; 3554 $settings = $this->theme_json['settings'] ?? array(); 3555 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; 3556 3557 /* 3558 * If there are content and wide widths in theme.json, output them 3559 * as custom properties on the body element so all blocks can use them. 3560 */ 3561 if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { 3562 $content_size = $settings['layout']['contentSize'] ?? $settings['layout']['wideSize']; 3563 $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; 3564 $wide_size = $settings['layout']['wideSize'] ?? $settings['layout']['contentSize']; 3565 $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; 3566 $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';'; 3567 $css .= '--wp--style--global--wide-size: ' . $wide_size . '; }'; 3568 } 3569 3570 /* 3571 * Reset default browser margin on the body element. 3572 * This is set on the body selector **before** generating the ruleset 3573 * from the `theme.json`. This is to ensure that if the `theme.json` declares 3574 * `margin` in its `spacing` declaration for the `body` element then these 3575 * user-generated values take precedence in the CSS cascade. 3576 * @link https://github.com/WordPress/gutenberg/issues/36147. 3577 */ 3578 $css .= ':where(body) { margin: 0; }'; 3579 3580 if ( $use_root_padding ) { 3581 // Top and bottom padding are applied to the outer block container. 3582 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; 3583 // Right and left padding are applied to the first container with `.has-global-padding` class. 3584 $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; 3585 // Alignfull children of the container with left and right padding have negative margins so they can still be full width. 3586 $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); }'; 3587 // Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container. 3588 $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }'; 3589 // Alignfull direct children of the containers that are targeted by the rule above do not need negative margins. 3590 $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }'; 3591 } 3592 3593 // Skip outputting alignment styles when base_layout_styles is enabled. 3594 // These styles target .wp-site-blocks which is only used by block themes. 3595 if ( empty( $options['base_layout_styles'] ) ) { 3596 $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; 3597 $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; 3598 $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; 3599 } 3600 3601 // Block gap styles will be output unless explicitly set to `null`. 3602 if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) { 3603 $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); 3604 $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }"; 3605 $css .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }'; 3606 $css .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }'; 3607 3608 // For backwards compatibility, ensure the legacy block gap CSS variable is still available. 3609 $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }"; 3610 } 3611 $css .= $this->get_layout_styles( $block_metadata, $options ); 3612 3613 return $css; 3614 } 3615 3616 /** 3617 * For metadata values that can either be booleans or paths to booleans, gets the value. 3618 * 3619 * $data = array( 3620 * 'color' => array( 3621 * 'defaultPalette' => true 3622 * ) 3623 * ); 3624 * 3625 * static::get_metadata_boolean( $data, false ); 3626 * // => false 3627 * 3628 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); 3629 * // => true 3630 * 3631 * @since 6.0.0 3632 * 3633 * @param array $data The data to inspect. 3634 * @param bool|array $path Boolean or path to a boolean. 3635 * @param bool $default_value Default value if the referenced path is missing. 3636 * Default false. 3637 * @return bool Value of boolean metadata. 3638 */ 3639 protected static function get_metadata_boolean( $data, $path, $default_value = false ) { 3640 if ( is_bool( $path ) ) { 3641 return $path; 3642 } 3643 3644 if ( is_array( $path ) ) { 3645 $value = _wp_array_get( $data, $path ); 3646 if ( null !== $value ) { 3647 return $value; 3648 } 3649 } 3650 3651 return $default_value; 3652 } 3653 3654 /** 3655 * Merges new incoming data. 3656 * 3657 * @since 5.8.0 3658 * @since 5.9.0 Duotone preset also has origins. 3659 * @since 6.7.0 Replace background image objects during merge. 3660 * 3661 * @param WP_Theme_JSON $incoming Data to merge. 3662 */ 3663 public function merge( $incoming ) { 3664 $incoming_data = $incoming->get_raw_data(); 3665 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); 3666 3667 /* 3668 * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor 3669 * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so 3670 * we can allow partial spacingScale data to inherit missing data from earlier layers when 3671 * computing the spacing sizes. 3672 * 3673 * This happens before the presets are merged to ensure that default spacing sizes can be 3674 * removed from the theme origin if $prevent_override is true. 3675 */ 3676 $flattened_spacing_scale = array(); 3677 foreach ( static::VALID_ORIGINS as $origin ) { 3678 $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); 3679 3680 // Apply the base spacing scale to the current layer. 3681 $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() ); 3682 $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale ); 3683 3684 $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null ); 3685 if ( ! isset( $spacing_scale ) ) { 3686 continue; 3687 } 3688 3689 // Allow partial scale settings by merging with lower layers. 3690 $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale ); 3691 3692 // Generate and merge the scales for this layer. 3693 $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); 3694 $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() ); 3695 $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale ); 3696 $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); 3697 3698 _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes ); 3699 } 3700 3701 /* 3702 * The array_replace_recursive algorithm merges at the leaf level, 3703 * but we don't want leaf arrays to be merged, so we overwrite it. 3704 * 3705 * For leaf values that are sequential arrays it will use the numeric indexes for replacement. 3706 * We rather replace the existing with the incoming value, if it exists. 3707 * This is the case of spacing.units. 3708 * 3709 * For leaf values that are associative arrays it will merge them as expected. 3710 * This is also not the behavior we want for the current associative arrays (presets). 3711 * We rather replace the existing with the incoming value, if it exists. 3712 * This happens, for example, when we merge data from theme.json upon existing 3713 * theme supports or when we merge anything coming from the same source twice. 3714 * This is the case of color.palette, color.gradients, color.duotone, 3715 * typography.fontSizes, or typography.fontFamilies. 3716 * 3717 * Additionally, for some preset types, we also want to make sure the 3718 * values they introduce don't conflict with default values. We do so 3719 * by checking the incoming slugs for theme presets and compare them 3720 * with the equivalent default presets: if a slug is present as a default 3721 * we remove it from the theme presets. 3722 */ 3723 $nodes = static::get_setting_nodes( $incoming_data ); 3724 $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); 3725 foreach ( $nodes as $node ) { 3726 // Replace the spacing.units. 3727 $path = $node['path']; 3728 $path[] = 'spacing'; 3729 $path[] = 'units'; 3730 3731 $content = _wp_array_get( $incoming_data, $path, null ); 3732 if ( isset( $content ) ) { 3733 _wp_array_set( $this->theme_json, $path, $content ); 3734 } 3735 3736 // Replace the presets. 3737 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 3738 $prevent_override = $preset_metadata['prevent_override']; 3739 if ( is_array( $prevent_override ) ) { 3740 $global_path = array_merge( array( 'settings' ), $prevent_override ); 3741 $global_value = _wp_array_get( $this->theme_json, $global_path, null ); 3742 3743 $node_level_path = array_merge( $node['path'], $prevent_override ); 3744 $prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value ); 3745 } 3746 3747 foreach ( static::VALID_ORIGINS as $origin ) { 3748 $base_path = $node['path']; 3749 foreach ( $preset_metadata['path'] as $leaf ) { 3750 $base_path[] = $leaf; 3751 } 3752 3753 $path = $base_path; 3754 $path[] = $origin; 3755 3756 $content = _wp_array_get( $incoming_data, $path, null ); 3757 if ( ! isset( $content ) ) { 3758 continue; 3759 } 3760 3761 // Set names for theme presets based on the slug if they are not set and can use default names. 3762 if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) { 3763 foreach ( $content as $key => $item ) { 3764 if ( ! isset( $item['name'] ) ) { 3765 $name = static::get_name_from_defaults( $item['slug'], $base_path ); 3766 if ( null !== $name ) { 3767 $content[ $key ]['name'] = $name; 3768 } 3769 } 3770 } 3771 } 3772 3773 // Filter out default slugs from theme presets when defaults should not be overridden. 3774 if ( 'theme' === $origin && $prevent_override ) { 3775 $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); 3776 $preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() ); 3777 $preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() ); 3778 $preset_slugs = array_merge_recursive( $preset_global, $preset_node ); 3779 3780 $content = static::filter_slugs( $content, $preset_slugs ); 3781 } 3782 3783 _wp_array_set( $this->theme_json, $path, $content ); 3784 } 3785 } 3786 } 3787 3788 /* 3789 * Style values are merged at the leaf level, however 3790 * some values provide exceptions, namely style values that are 3791 * objects and represent unique definitions for the style. 3792 */ 3793 $style_nodes = static::get_block_nodes( 3794 $this->theme_json, 3795 array(), 3796 array( 'include_node_paths_only' => true ) 3797 ); 3798 3799 // Add top-level styles. 3800 $style_nodes[] = array( 'path' => array( 'styles' ) ); 3801 3802 foreach ( $style_nodes as $style_node ) { 3803 $path = $style_node['path']; 3804 /* 3805 * Background image styles should be replaced, not merged, 3806 * as they themselves are specific object definitions for the style. 3807 */ 3808 $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] ); 3809 $content = _wp_array_get( $incoming_data, $background_image_path, null ); 3810 if ( isset( $content ) ) { 3811 _wp_array_set( $this->theme_json, $background_image_path, $content ); 3812 } 3813 } 3814 } 3815 3816 /** 3817 * Converts all filter (duotone) presets into SVGs. 3818 * 3819 * @since 5.9.1 3820 * 3821 * @param array $origins List of origins to process. 3822 * @return string SVG filters. 3823 */ 3824 public function get_svg_filters( $origins ) { 3825 $blocks_metadata = static::get_blocks_metadata(); 3826 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); 3827 3828 $filters = ''; 3829 foreach ( $setting_nodes as $metadata ) { 3830 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 3831 if ( empty( $node['color']['duotone'] ) ) { 3832 continue; 3833 } 3834 3835 $duotone_presets = $node['color']['duotone']; 3836 3837 foreach ( $origins as $origin ) { 3838 if ( ! isset( $duotone_presets[ $origin ] ) ) { 3839 continue; 3840 } 3841 foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { 3842 $filters .= WP_Duotone::get_filter_svg_from_preset( $duotone_preset ); 3843 } 3844 } 3845 } 3846 3847 return $filters; 3848 } 3849 3850 /** 3851 * Determines whether a presets should be overridden or not. 3852 * 3853 * @since 5.9.0 3854 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. 3855 * 3856 * @param array $theme_json The theme.json like structure to inspect. 3857 * @param array $path Path to inspect. 3858 * @param bool|array $override Data to compute whether to override the preset. 3859 * @return bool|null True if the preset should override the defaults, false if not. Null if the override parameter is invalid. 3860 */ 3861 protected static function should_override_preset( $theme_json, $path, $override ) { 3862 _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); 3863 3864 if ( is_bool( $override ) ) { 3865 return $override; 3866 } 3867 3868 /* 3869 * The relationship between whether to override the defaults 3870 * and whether the defaults are enabled is inverse: 3871 * 3872 * - If defaults are enabled => theme presets should not be overridden 3873 * - If defaults are disabled => theme presets should be overridden 3874 * 3875 * For example, a theme sets defaultPalette to false, 3876 * making the default palette hidden from the user. 3877 * In that case, we want all the theme presets to be present, 3878 * so they should override the defaults. 3879 */ 3880 if ( is_array( $override ) ) { 3881 $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); 3882 if ( isset( $value ) ) { 3883 return ! $value; 3884 } 3885 3886 // Search the top-level key if none was found for this node. 3887 $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); 3888 if ( isset( $value ) ) { 3889 return ! $value; 3890 } 3891 3892 return true; 3893 } 3894 3895 return null; 3896 } 3897 3898 /** 3899 * Returns the default slugs for all the presets in an associative array 3900 * whose keys are the preset paths and the leaves is the list of slugs. 3901 * 3902 * For example: 3903 * 3904 * array( 3905 * 'color' => array( 3906 * 'palette' => array( 'slug-1', 'slug-2' ), 3907 * 'gradients' => array( 'slug-3', 'slug-4' ), 3908 * ), 3909 * ) 3910 * 3911 * @since 5.9.0 3912 * 3913 * @param array $data A theme.json like structure. 3914 * @param array $node_path The path to inspect. It's 'settings' by default. 3915 * @return array 3916 */ 3917 protected static function get_default_slugs( $data, $node_path ) { 3918 $slugs = array(); 3919 3920 foreach ( static::PRESETS_METADATA as $metadata ) { 3921 $path = $node_path; 3922 foreach ( $metadata['path'] as $leaf ) { 3923 $path[] = $leaf; 3924 } 3925 $path[] = 'default'; 3926 3927 $preset = _wp_array_get( $data, $path, null ); 3928 if ( ! isset( $preset ) ) { 3929 continue; 3930 } 3931 3932 $slugs_for_preset = array(); 3933 foreach ( $preset as $item ) { 3934 if ( isset( $item['slug'] ) ) { 3935 $slugs_for_preset[] = $item['slug']; 3936 } 3937 } 3938 3939 _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); 3940 } 3941 3942 return $slugs; 3943 } 3944 3945 /** 3946 * Gets a `default`'s preset name by a provided slug. 3947 * 3948 * @since 5.9.0 3949 * 3950 * @param string $slug The slug we want to find a match from default presets. 3951 * @param array $base_path The path to inspect. It's 'settings' by default. 3952 * @return string|null 3953 */ 3954 protected function get_name_from_defaults( $slug, $base_path ) { 3955 $path = $base_path; 3956 $path[] = 'default'; 3957 $default_content = _wp_array_get( $this->theme_json, $path, null ); 3958 if ( ! $default_content ) { 3959 return null; 3960 } 3961 foreach ( $default_content as $item ) { 3962 if ( $slug === $item['slug'] ) { 3963 return $item['name']; 3964 } 3965 } 3966 return null; 3967 } 3968 3969 /** 3970 * Removes the preset values whose slug is equal to any of given slugs. 3971 * 3972 * @since 5.9.0 3973 * 3974 * @param array $node The node with the presets to validate. 3975 * @param array $slugs The slugs that should not be overridden. 3976 * @return array The new node. 3977 */ 3978 protected static function filter_slugs( $node, $slugs ) { 3979 if ( empty( $slugs ) ) { 3980 return $node; 3981 } 3982 3983 $new_node = array(); 3984 foreach ( $node as $value ) { 3985 if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { 3986 $new_node[] = $value; 3987 } 3988 } 3989 3990 return $new_node; 3991 } 3992 3993 /** 3994 * Removes insecure data from theme.json. 3995 * 3996 * @since 5.9.0 3997 * @since 6.3.2 Preserves global styles block variations when securing styles. 3998 * @since 6.6.0 Updated to allow variation element styles and $origin parameter. 3999 * 4000 * @param array $theme_json Structure to sanitize. 4001 * @param string $origin Optional. What source of data this object represents. 4002 * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'. 4003 * @return array Sanitized structure. 4004 */ 4005 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) { 4006 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { 4007 $origin = 'theme'; 4008 } 4009 4010 $sanitized = array(); 4011 4012 $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin ); 4013 4014 $blocks_metadata = static::get_blocks_metadata(); 4015 $valid_block_names = array_keys( $blocks_metadata ); 4016 $valid_element_names = array_keys( static::ELEMENTS ); 4017 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata ); 4018 4019 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations ); 4020 4021 $blocks_metadata = static::get_blocks_metadata(); 4022 $style_options = array( 'include_block_style_variations' => true ); // Allow variations data. 4023 $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options ); 4024 4025 foreach ( $style_nodes as $metadata ) { 4026 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 4027 if ( empty( $input ) ) { 4028 continue; 4029 } 4030 4031 $block_name = in_array( 'blocks', $metadata['path'], true ) 4032 ? static::get_block_name_from_metadata_path( $metadata ) 4033 : null; 4034 4035 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. 4036 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { 4037 $output = $input; 4038 } else { 4039 $output = static::remove_insecure_styles( $input ); 4040 } 4041 4042 /* 4043 * Get a reference to element name from path. 4044 * $metadata['path'] = array( 'styles', 'elements', 'link' ); 4045 */ 4046 $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; 4047 4048 /* 4049 * $output is stripped of pseudo selectors. Re-add and process them 4050 * or insecure styles here. 4051 */ 4052 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { 4053 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { 4054 if ( isset( $input[ $pseudo_selector ] ) ) { 4055 $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); 4056 } 4057 } 4058 } 4059 4060 // Re-add and process responsive breakpoint styles. 4061 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4062 if ( isset( $input[ $breakpoint ] ) ) { 4063 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] ); 4064 4065 if ( isset( $input[ $breakpoint ]['elements'] ) ) { 4066 $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] ); 4067 } 4068 4069 if ( isset( $input[ $breakpoint ]['blocks'] ) ) { 4070 $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] ); 4071 } 4072 4073 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { 4074 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { 4075 if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) { 4076 $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] ); 4077 } 4078 } 4079 } 4080 4081 // Responsive custom CSS is allowed for users with 'edit_css' capability. 4082 if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { 4083 $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css']; 4084 } 4085 } 4086 } 4087 4088 if ( ! empty( $output ) ) { 4089 _wp_array_set( $sanitized, $metadata['path'], $output ); 4090 } 4091 4092 if ( isset( $metadata['variations'] ) ) { 4093 foreach ( $metadata['variations'] as $variation ) { 4094 $variation_input = _wp_array_get( $theme_json, $variation['path'], array() ); 4095 if ( empty( $variation_input ) ) { 4096 continue; 4097 } 4098 4099 $variation_output = static::remove_insecure_styles( $variation_input ); 4100 4101 if ( isset( $variation_input['blocks'] ) ) { 4102 $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] ); 4103 } 4104 4105 if ( isset( $variation_input['elements'] ) ) { 4106 $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] ); 4107 } 4108 4109 // Re-add and process responsive breakpoint styles for variations. 4110 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4111 if ( isset( $variation_input[ $breakpoint ] ) ) { 4112 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] ); 4113 4114 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) { 4115 $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] ); 4116 } 4117 4118 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) { 4119 $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] ); 4120 } 4121 4122 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { 4123 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { 4124 if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4125 $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] ); 4126 } 4127 } 4128 } 4129 4130 // Responsive custom CSS is allowed for users with 'edit_css' capability. 4131 if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { 4132 $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css']; 4133 } 4134 } 4135 } 4136 4137 if ( ! empty( $variation_output ) ) { 4138 _wp_array_set( $sanitized, $variation['path'], $variation_output ); 4139 } 4140 } 4141 } 4142 } 4143 4144 $setting_nodes = static::get_setting_nodes( $theme_json ); 4145 foreach ( $setting_nodes as $metadata ) { 4146 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 4147 if ( empty( $input ) ) { 4148 continue; 4149 } 4150 4151 $output = static::remove_insecure_settings( $input ); 4152 if ( ! empty( $output ) ) { 4153 _wp_array_set( $sanitized, $metadata['path'], $output ); 4154 } 4155 } 4156 4157 if ( empty( $sanitized['styles'] ) ) { 4158 unset( $theme_json['styles'] ); 4159 } else { 4160 $theme_json['styles'] = $sanitized['styles']; 4161 } 4162 4163 if ( empty( $sanitized['settings'] ) ) { 4164 unset( $theme_json['settings'] ); 4165 } else { 4166 $theme_json['settings'] = $sanitized['settings']; 4167 } 4168 4169 return $theme_json; 4170 } 4171 4172 /** 4173 * Remove insecure element styles within a variation or block. 4174 * 4175 * @since 6.8.0 4176 * 4177 * @param array $elements The elements to process. 4178 * @return array The sanitized elements styles. 4179 */ 4180 protected static function remove_insecure_element_styles( $elements ) { 4181 $sanitized = array(); 4182 $valid_element_names = array_keys( static::ELEMENTS ); 4183 4184 foreach ( $valid_element_names as $element_name ) { 4185 $element_input = $elements[ $element_name ] ?? null; 4186 if ( $element_input ) { 4187 $element_output = static::remove_insecure_styles( $element_input ); 4188 4189 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { 4190 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { 4191 if ( isset( $element_input[ $pseudo_selector ] ) ) { 4192 $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] ); 4193 } 4194 } 4195 } 4196 4197 // Re-add and process responsive breakpoint styles for elements. 4198 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4199 if ( isset( $element_input[ $breakpoint ] ) ) { 4200 $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] ); 4201 4202 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { 4203 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { 4204 if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4205 $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] ); 4206 } 4207 } 4208 } 4209 } 4210 } 4211 4212 $sanitized[ $element_name ] = $element_output; 4213 } 4214 } 4215 return $sanitized; 4216 } 4217 4218 /** 4219 * Remove insecure styles from inner blocks and their elements. 4220 * 4221 * @since 6.8.0 4222 * 4223 * @param array $blocks The block styles to process. 4224 * @return array Sanitized block type styles. 4225 */ 4226 protected static function remove_insecure_inner_block_styles( $blocks ) { 4227 $sanitized = array(); 4228 foreach ( $blocks as $block_type => $block_input ) { 4229 $block_output = static::remove_insecure_styles( $block_input ); 4230 4231 if ( isset( $block_input['elements'] ) ) { 4232 $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] ); 4233 } 4234 4235 // Re-add and process responsive breakpoint styles for inner blocks. 4236 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4237 if ( isset( $block_input[ $breakpoint ] ) ) { 4238 $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] ); 4239 4240 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) { 4241 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) { 4242 if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4243 $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] ); 4244 } 4245 } 4246 } 4247 } 4248 } 4249 4250 $sanitized[ $block_type ] = $block_output; 4251 } 4252 return $sanitized; 4253 } 4254 4255 /** 4256 * Preserves valid typed settings from input to output based on type markers in schema. 4257 * 4258 * Recursively iterates through the schema and validates/preserves settings 4259 * that have type markers (e.g., boolean) in VALID_SETTINGS. 4260 * 4261 * @since 7.0.0 4262 * 4263 * @param array $input Input settings to process. 4264 * @param array $output Output settings array (passed by reference). 4265 * @param array $schema Schema to validate against (typically VALID_SETTINGS). 4266 * @param array<string|int> $path Current path in the schema (for recursive calls). 4267 */ 4268 private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) { 4269 foreach ( $schema as $key => $schema_value ) { 4270 $current_path = array_merge( $path, array( $key ) ); 4271 4272 // Validate boolean type markers. 4273 if ( is_bool( $schema_value ) ) { 4274 $value = _wp_array_get( $input, $current_path, null ); 4275 if ( is_bool( $value ) ) { 4276 _wp_array_set( $output, $current_path, $value ); // Preserve boolean value. 4277 } 4278 } elseif ( is_array( $schema_value ) ) { 4279 self::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure. 4280 } 4281 } 4282 } 4283 4284 /** 4285 * Processes a setting node and returns the same node 4286 * without the insecure settings. 4287 * 4288 * @since 5.9.0 4289 * 4290 * @param array $input Node to process. 4291 * @return array 4292 */ 4293 protected static function remove_insecure_settings( $input ) { 4294 $output = array(); 4295 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 4296 foreach ( static::VALID_ORIGINS as $origin ) { 4297 $path_with_origin = $preset_metadata['path']; 4298 $path_with_origin[] = $origin; 4299 $presets = _wp_array_get( $input, $path_with_origin, null ); 4300 if ( null === $presets ) { 4301 continue; 4302 } 4303 4304 $escaped_preset = array(); 4305 foreach ( $presets as $preset ) { 4306 if ( 4307 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && 4308 sanitize_html_class( $preset['slug'] ) === $preset['slug'] 4309 ) { 4310 $value = null; 4311 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { 4312 $value = $preset[ $preset_metadata['value_key'] ]; 4313 } elseif ( 4314 isset( $preset_metadata['value_func'] ) && 4315 is_callable( $preset_metadata['value_func'] ) 4316 ) { 4317 $value = call_user_func( $preset_metadata['value_func'], $preset ); 4318 } 4319 4320 $preset_is_valid = true; 4321 foreach ( $preset_metadata['properties'] as $property ) { 4322 if ( ! static::is_safe_css_declaration( $property, $value ) ) { 4323 $preset_is_valid = false; 4324 break; 4325 } 4326 } 4327 4328 if ( $preset_is_valid ) { 4329 $escaped_preset[] = $preset; 4330 } 4331 } 4332 } 4333 4334 if ( ! empty( $escaped_preset ) ) { 4335 _wp_array_set( $output, $path_with_origin, $escaped_preset ); 4336 } 4337 } 4338 } 4339 4340 // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed. 4341 static::remove_indirect_properties( $input, $output ); 4342 4343 // Preserve all valid settings that have type markers in VALID_SETTINGS. 4344 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS ); 4345 4346 return $output; 4347 } 4348 4349 /** 4350 * Processes a style node and returns the same node 4351 * without the insecure styles. 4352 * 4353 * @since 5.9.0 4354 * 4355 * @param array $input Node to process. 4356 * @return array 4357 */ 4358 protected static function remove_insecure_styles( $input ) { 4359 $output = array(); 4360 $declarations = static::compute_style_properties( $input ); 4361 4362 foreach ( $declarations as $declaration ) { 4363 if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { 4364 $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; 4365 4366 /* 4367 * Check the value isn't an array before adding so as to not 4368 * double up shorthand and longhand styles. 4369 */ 4370 $value = _wp_array_get( $input, $path, array() ); 4371 if ( ! is_array( $value ) ) { 4372 _wp_array_set( $output, $path, $value ); 4373 } 4374 } 4375 } 4376 4377 // Ensure indirect properties not handled by `compute_style_properties` are allowed. 4378 static::remove_indirect_properties( $input, $output ); 4379 4380 return $output; 4381 } 4382 4383 /** 4384 * Checks that a declaration provided by the user is safe. 4385 * 4386 * @since 5.9.0 4387 * 4388 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. 4389 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. 4390 * @return bool 4391 */ 4392 protected static function is_safe_css_declaration( $property_name, $property_value ) { 4393 $style_to_validate = $property_name . ': ' . $property_value; 4394 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); 4395 return ! empty( trim( $filtered ) ); 4396 } 4397 4398 /** 4399 * Removes indirect properties from the given input node and 4400 * sets in the given output node. 4401 * 4402 * @since 6.2.0 4403 * 4404 * @param array $input Node to process. 4405 * @param array $output The processed node. Passed by reference. 4406 */ 4407 private static function remove_indirect_properties( $input, &$output ) { 4408 foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { 4409 foreach ( $paths as $path ) { 4410 $value = _wp_array_get( $input, $path ); 4411 if ( 4412 is_string( $value ) && 4413 static::is_safe_css_declaration( $property, $value ) 4414 ) { 4415 _wp_array_set( $output, $path, $value ); 4416 } 4417 } 4418 } 4419 } 4420 4421 /** 4422 * Returns the raw data. 4423 * 4424 * @since 5.8.0 4425 * 4426 * @return array Raw data. 4427 */ 4428 public function get_raw_data() { 4429 return $this->theme_json; 4430 } 4431 4432 /** 4433 * Transforms the given editor settings according the 4434 * add_theme_support format to the theme.json format. 4435 * 4436 * @since 5.8.0 4437 * 4438 * @param array $settings Existing editor settings. 4439 * @return array Config that adheres to the theme.json schema. 4440 */ 4441 public static function get_from_editor_settings( $settings ) { 4442 $theme_settings = array( 4443 'version' => static::LATEST_SCHEMA, 4444 'settings' => array(), 4445 ); 4446 4447 // Deprecated theme supports. 4448 if ( isset( $settings['disableCustomColors'] ) ) { 4449 $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; 4450 } 4451 4452 if ( isset( $settings['disableCustomGradients'] ) ) { 4453 $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; 4454 } 4455 4456 if ( isset( $settings['disableCustomFontSizes'] ) ) { 4457 $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; 4458 } 4459 4460 if ( isset( $settings['enableCustomLineHeight'] ) ) { 4461 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; 4462 } 4463 4464 if ( isset( $settings['enableCustomUnits'] ) ) { 4465 $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? 4466 array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : 4467 $settings['enableCustomUnits']; 4468 } 4469 4470 if ( isset( $settings['colors'] ) ) { 4471 $theme_settings['settings']['color']['palette'] = $settings['colors']; 4472 } 4473 4474 if ( isset( $settings['gradients'] ) ) { 4475 $theme_settings['settings']['color']['gradients'] = $settings['gradients']; 4476 } 4477 4478 if ( isset( $settings['fontSizes'] ) ) { 4479 $font_sizes = $settings['fontSizes']; 4480 // Back-compatibility for presets without units. 4481 foreach ( $font_sizes as $key => $font_size ) { 4482 if ( is_numeric( $font_size['size'] ) ) { 4483 $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; 4484 } 4485 } 4486 $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; 4487 } 4488 4489 if ( isset( $settings['enableCustomSpacing'] ) ) { 4490 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; 4491 } 4492 4493 if ( isset( $settings['spacingSizes'] ) ) { 4494 $theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes']; 4495 } 4496 4497 return $theme_settings; 4498 } 4499 4500 /** 4501 * Returns the current theme's wanted patterns(slugs) to be 4502 * registered from Pattern Directory. 4503 * 4504 * @since 6.0.0 4505 * 4506 * @return string[] 4507 */ 4508 public function get_patterns() { 4509 if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { 4510 return $this->theme_json['patterns']; 4511 } 4512 return array(); 4513 } 4514 4515 /** 4516 * Returns a valid theme.json as provided by a theme. 4517 * 4518 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. 4519 * This also uses appearanceTools instead of their opt-ins if all of them are true. 4520 * 4521 * @since 6.0.0 4522 * 4523 * @return array 4524 */ 4525 public function get_data() { 4526 $output = $this->theme_json; 4527 $nodes = static::get_setting_nodes( $output ); 4528 4529 /** 4530 * Flatten the theme & custom origins into a single one. 4531 * 4532 * For example, the following: 4533 * 4534 * { 4535 * "settings": { 4536 * "color": { 4537 * "palette": { 4538 * "theme": [ {} ], 4539 * "custom": [ {} ] 4540 * } 4541 * } 4542 * } 4543 * } 4544 * 4545 * will be converted to: 4546 * 4547 * { 4548 * "settings": { 4549 * "color": { 4550 * "palette": [ {} ] 4551 * } 4552 * } 4553 * } 4554 */ 4555 foreach ( $nodes as $node ) { 4556 foreach ( static::PRESETS_METADATA as $preset_metadata ) { 4557 $path = $node['path']; 4558 foreach ( $preset_metadata['path'] as $preset_metadata_path ) { 4559 $path[] = $preset_metadata_path; 4560 } 4561 $preset = _wp_array_get( $output, $path, null ); 4562 if ( null === $preset ) { 4563 continue; 4564 } 4565 4566 $items = array(); 4567 if ( isset( $preset['theme'] ) ) { 4568 foreach ( $preset['theme'] as $item ) { 4569 $slug = $item['slug']; 4570 unset( $item['slug'] ); 4571 $items[ $slug ] = $item; 4572 } 4573 } 4574 if ( isset( $preset['custom'] ) ) { 4575 foreach ( $preset['custom'] as $item ) { 4576 $slug = $item['slug']; 4577 unset( $item['slug'] ); 4578 $items[ $slug ] = $item; 4579 } 4580 } 4581 $flattened_preset = array(); 4582 foreach ( $items as $slug => $value ) { 4583 $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); 4584 } 4585 _wp_array_set( $output, $path, $flattened_preset ); 4586 } 4587 } 4588 4589 /* 4590 * If all of the static::APPEARANCE_TOOLS_OPT_INS are true, 4591 * this code unsets them and sets 'appearanceTools' instead. 4592 */ 4593 foreach ( $nodes as $node ) { 4594 $all_opt_ins_are_set = true; 4595 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { 4596 $full_path = $node['path']; 4597 foreach ( $opt_in_path as $opt_in_path_item ) { 4598 $full_path[] = $opt_in_path_item; 4599 } 4600 /* 4601 * Use "unset prop" as a marker instead of "null" because 4602 * "null" can be a valid value for some props (e.g. blockGap). 4603 */ 4604 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); 4605 if ( 'unset prop' === $opt_in_value ) { 4606 $all_opt_ins_are_set = false; 4607 break; 4608 } 4609 } 4610 4611 if ( $all_opt_ins_are_set ) { 4612 $node_path_with_appearance_tools = $node['path']; 4613 $node_path_with_appearance_tools[] = 'appearanceTools'; 4614 _wp_array_set( $output, $node_path_with_appearance_tools, true ); 4615 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { 4616 $full_path = $node['path']; 4617 foreach ( $opt_in_path as $opt_in_path_item ) { 4618 $full_path[] = $opt_in_path_item; 4619 } 4620 /* 4621 * Use "unset prop" as a marker instead of "null" because 4622 * "null" can be a valid value for some props (e.g. blockGap). 4623 */ 4624 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); 4625 if ( true !== $opt_in_value ) { 4626 continue; 4627 } 4628 4629 /* 4630 * The following could be improved to be path independent. 4631 * At the moment it relies on a couple of assumptions: 4632 * 4633 * - all opt-ins having a path of size 2. 4634 * - there's two sources of settings: the top-level and the block-level. 4635 */ 4636 if ( 4637 ( 1 === count( $node['path'] ) ) && 4638 ( 'settings' === $node['path'][0] ) 4639 ) { 4640 // Top-level settings. 4641 unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); 4642 if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { 4643 unset( $output['settings'][ $opt_in_path[0] ] ); 4644 } 4645 } elseif ( 4646 ( 3 === count( $node['path'] ) ) && 4647 ( 'settings' === $node['path'][0] ) && 4648 ( 'blocks' === $node['path'][1] ) 4649 ) { 4650 // Block-level settings. 4651 $block_name = $node['path'][2]; 4652 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); 4653 if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { 4654 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); 4655 } 4656 } 4657 } 4658 } 4659 } 4660 4661 wp_recursive_ksort( $output ); 4662 4663 return $output; 4664 } 4665 4666 /** 4667 * Sets the spacingSizes array based on the spacingScale values from theme.json. 4668 * 4669 * @since 6.1.0 4670 * @deprecated 6.6.0 No longer used as the spacingSizes are automatically 4671 * generated in the constructor and merge methods instead 4672 * of manually after instantiation. 4673 * 4674 * @return void 4675 */ 4676 public function set_spacing_sizes() { 4677 _deprecated_function( __METHOD__, '6.6.0' ); 4678 4679 $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array(); 4680 4681 if ( ! isset( $spacing_scale['steps'] ) 4682 || ! is_numeric( $spacing_scale['steps'] ) 4683 || ! isset( $spacing_scale['mediumStep'] ) 4684 || ! isset( $spacing_scale['unit'] ) 4685 || ! isset( $spacing_scale['operator'] ) 4686 || ! isset( $spacing_scale['increment'] ) 4687 || ! isset( $spacing_scale['steps'] ) 4688 || ! is_numeric( $spacing_scale['increment'] ) 4689 || ! is_numeric( $spacing_scale['mediumStep'] ) 4690 || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { 4691 if ( ! empty( $spacing_scale ) ) { 4692 wp_trigger_error( 4693 __METHOD__, 4694 sprintf( 4695 /* translators: 1: theme.json, 2: settings.spacing.spacingScale */ 4696 __( 'Some of the %1$s %2$s values are invalid' ), 4697 'theme.json', 4698 'settings.spacing.spacingScale' 4699 ), 4700 E_USER_NOTICE 4701 ); 4702 } 4703 return; 4704 } 4705 4706 // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0. 4707 if ( 0 === $spacing_scale['steps'] ) { 4708 return; 4709 } 4710 4711 $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); 4712 4713 // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. 4714 if ( $spacing_scale['steps'] <= 7 ) { 4715 for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { 4716 $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); 4717 } 4718 } 4719 4720 _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); 4721 } 4722 4723 /** 4724 * Merges two sets of spacing size presets. 4725 * 4726 * @since 6.6.0 4727 * 4728 * @param array $base The base set of spacing sizes. 4729 * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values. 4730 * @return array The merged set of spacing sizes. 4731 */ 4732 private static function merge_spacing_sizes( $base, $incoming ) { 4733 // Preserve the order if there are no base (spacingScale) values. 4734 if ( empty( $base ) ) { 4735 return $incoming; 4736 } 4737 $merged = array(); 4738 foreach ( $base as $item ) { 4739 $merged[ $item['slug'] ] = $item; 4740 } 4741 foreach ( $incoming as $item ) { 4742 $merged[ $item['slug'] ] = $item; 4743 } 4744 ksort( $merged, SORT_NUMERIC ); 4745 return array_values( $merged ); 4746 } 4747 4748 /** 4749 * Generates a set of spacing sizes by starting with a medium size and 4750 * applying an operator with an increment value to generate the rest of the 4751 * sizes outward from the medium size. The medium slug is '50' with the rest 4752 * of the slugs being 10 apart. The generated names use t-shirt sizing. 4753 * 4754 * Example: 4755 * 4756 * $spacing_scale = array( 4757 * 'steps' => 4, 4758 * 'mediumStep' => 16, 4759 * 'unit' => 'px', 4760 * 'operator' => '+', 4761 * 'increment' => 2, 4762 * ); 4763 * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); 4764 * // -> array( 4765 * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ), 4766 * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ), 4767 * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ), 4768 * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ), 4769 * // ) 4770 * 4771 * @since 6.6.0 4772 * 4773 * @param array $spacing_scale { 4774 * The spacing scale values. All are required. 4775 * 4776 * @type int $steps The number of steps in the scale. (up to 10 steps are supported.) 4777 * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.) 4778 * @type string $unit The CSS unit to use for the sizes. 4779 * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'. 4780 * @type float $increment The value used with the operator to generate the other sizes. 4781 * } 4782 * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid. 4783 */ 4784 private static function compute_spacing_sizes( $spacing_scale ) { 4785 /* 4786 * This condition is intentionally missing some checks on ranges for the values in order to 4787 * keep backwards compatibility with the previous implementation. 4788 */ 4789 if ( 4790 ! isset( $spacing_scale['steps'] ) || 4791 ! is_numeric( $spacing_scale['steps'] ) || 4792 0 === $spacing_scale['steps'] || 4793 ! isset( $spacing_scale['mediumStep'] ) || 4794 ! is_numeric( $spacing_scale['mediumStep'] ) || 4795 ! isset( $spacing_scale['unit'] ) || 4796 ! isset( $spacing_scale['operator'] ) || 4797 ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) || 4798 ! isset( $spacing_scale['increment'] ) || 4799 ! is_numeric( $spacing_scale['increment'] ) 4800 ) { 4801 return array(); 4802 } 4803 4804 $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); 4805 $current_step = $spacing_scale['mediumStep']; 4806 $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); 4807 $x_small_count = null; 4808 $below_sizes = array(); 4809 $slug = 40; 4810 $remainder = 0; 4811 4812 for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) { 4813 if ( '+' === $spacing_scale['operator'] ) { 4814 $current_step -= $spacing_scale['increment']; 4815 } elseif ( $spacing_scale['increment'] > 1 ) { 4816 $current_step /= $spacing_scale['increment']; 4817 } else { 4818 $current_step *= $spacing_scale['increment']; 4819 } 4820 4821 if ( $current_step <= 0 ) { 4822 $remainder = $below_midpoint_count; 4823 break; 4824 } 4825 4826 $below_sizes[] = array( 4827 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ 4828 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ), 4829 'slug' => (string) $slug, 4830 'size' => round( $current_step, 2 ) . $unit, 4831 ); 4832 4833 if ( $below_midpoint_count === $steps_mid_point - 2 ) { 4834 $x_small_count = 2; 4835 } 4836 4837 if ( $below_midpoint_count < $steps_mid_point - 2 ) { 4838 ++$x_small_count; 4839 } 4840 4841 $slug -= 10; 4842 } 4843 4844 $below_sizes = array_reverse( $below_sizes ); 4845 4846 $below_sizes[] = array( 4847 'name' => __( 'Medium' ), 4848 'slug' => '50', 4849 'size' => $spacing_scale['mediumStep'] . $unit, 4850 ); 4851 4852 $current_step = $spacing_scale['mediumStep']; 4853 $x_large_count = null; 4854 $above_sizes = array(); 4855 $slug = 60; 4856 $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; 4857 4858 for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) { 4859 $current_step = '+' === $spacing_scale['operator'] 4860 ? $current_step + $spacing_scale['increment'] 4861 : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); 4862 4863 $above_sizes[] = array( 4864 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ 4865 'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ), 4866 'slug' => (string) $slug, 4867 'size' => round( $current_step, 2 ) . $unit, 4868 ); 4869 4870 if ( 1 === $above_midpoint_count ) { 4871 $x_large_count = 2; 4872 } 4873 4874 if ( $above_midpoint_count > 1 ) { 4875 ++$x_large_count; 4876 } 4877 4878 $slug += 10; 4879 } 4880 4881 $spacing_sizes = $below_sizes; 4882 foreach ( $above_sizes as $above_sizes_item ) { 4883 $spacing_sizes[] = $above_sizes_item; 4884 } 4885 4886 return $spacing_sizes; 4887 } 4888 4889 /** 4890 * This is used to convert the internal representation of variables to the CSS representation. 4891 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`. 4892 * 4893 * @since 6.3.0 4894 * 4895 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert. 4896 * @return string The converted variable. 4897 */ 4898 private static function convert_custom_properties( $value ) { 4899 $prefix = 'var:'; 4900 $prefix_len = strlen( $prefix ); 4901 $token_in = '|'; 4902 $token_out = '--'; 4903 if ( str_starts_with( $value, $prefix ) ) { 4904 $unwrapped_name = str_replace( 4905 $token_in, 4906 $token_out, 4907 substr( $value, $prefix_len ) 4908 ); 4909 $value = "var(--wp--$unwrapped_name)"; 4910 } 4911 4912 return $value; 4913 } 4914 4915 /** 4916 * Given a tree, converts the internal representation of variables to the CSS representation. 4917 * It is recursive and modifies the input in-place. 4918 * 4919 * @since 6.3.0 4920 * 4921 * @param array $tree Input to process. 4922 * @return array The modified $tree. 4923 */ 4924 private static function resolve_custom_css_format( $tree ) { 4925 $prefix = 'var:'; 4926 4927 foreach ( $tree as $key => $data ) { 4928 if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) { 4929 $tree[ $key ] = self::convert_custom_properties( $data ); 4930 } elseif ( is_array( $data ) ) { 4931 $tree[ $key ] = self::resolve_custom_css_format( $data ); 4932 } 4933 } 4934 4935 return $tree; 4936 } 4937 4938 /** 4939 * Returns the selectors metadata for a block. 4940 * 4941 * @since 6.3.0 4942 * 4943 * @param object $block_type The block type. 4944 * @param string $root_selector The block's root selector. 4945 * @return array The custom selectors set by the block. 4946 */ 4947 protected static function get_block_selectors( $block_type, $root_selector ) { 4948 if ( ! empty( $block_type->selectors ) ) { 4949 return $block_type->selectors; 4950 } 4951 4952 $selectors = array( 'root' => $root_selector ); 4953 foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { 4954 $feature_selector = wp_get_block_css_selector( $block_type, $key ); 4955 if ( null !== $feature_selector ) { 4956 $selectors[ $feature ] = array( 'root' => $feature_selector ); 4957 } 4958 } 4959 4960 return $selectors; 4961 } 4962 4963 /** 4964 * Generates all the element selectors for a block. 4965 * 4966 * @since 6.3.0 4967 * 4968 * @param string $root_selector The block's root CSS selector. 4969 * @return array The block's element selectors. 4970 */ 4971 protected static function get_block_element_selectors( $root_selector ) { 4972 /* 4973 * Assign defaults, then override those that the block sets by itself. 4974 * If the block selector is compounded, will append the element to each 4975 * individual block selector. 4976 */ 4977 $block_selectors = explode( ',', $root_selector ); 4978 $element_selectors = array(); 4979 foreach ( static::ELEMENTS as $el_name => $el_selector ) { 4980 $element_selector = array(); 4981 foreach ( $block_selectors as $selector ) { 4982 if ( $selector === $el_selector ) { 4983 $element_selector = array( $el_selector ); 4984 break; 4985 } 4986 $element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' ); 4987 } 4988 $element_selectors[ $el_name ] = implode( ',', $element_selector ); 4989 } 4990 4991 return $element_selectors; 4992 } 4993 4994 /** 4995 * Generates style declarations for a node's features e.g., color, border, 4996 * typography etc. that have custom selectors in their related block's 4997 * metadata. 4998 * 4999 * @since 6.3.0 5000 * 5001 * @param object $metadata The related block metadata containing selectors. 5002 * @param object $node A merged theme.json node for block or variation. 5003 * @return array The style declarations for the node's features with custom 5004 * selectors. 5005 */ 5006 protected function get_feature_declarations_for_node( $metadata, &$node ) { 5007 $declarations = array(); 5008 5009 if ( ! isset( $metadata['selectors'] ) ) { 5010 return $declarations; 5011 } 5012 5013 $settings = $this->theme_json['settings'] ?? array(); 5014 5015 foreach ( $metadata['selectors'] as $feature => $feature_selectors ) { 5016 /* 5017 * Skip if this is the block's root selector or the block doesn't 5018 * have any styles for the feature. 5019 */ 5020 if ( 'root' === $feature || empty( $node[ $feature ] ) ) { 5021 continue; 5022 } 5023 5024 if ( is_array( $feature_selectors ) ) { 5025 foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { 5026 if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) { 5027 continue; 5028 } 5029 5030 /* 5031 * Create temporary node containing only the subfeature data 5032 * to leverage existing `compute_style_properties` function. 5033 */ 5034 $subfeature_node = array( 5035 $feature => array( 5036 $subfeature => $node[ $feature ][ $subfeature ], 5037 ), 5038 ); 5039 5040 // Generate style declarations. 5041 $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json ); 5042 5043 // Merge subfeature declarations into feature declarations. 5044 if ( isset( $declarations[ $subfeature_selector ] ) ) { 5045 foreach ( $new_declarations as $new_declaration ) { 5046 $declarations[ $subfeature_selector ][] = $new_declaration; 5047 } 5048 } else { 5049 $declarations[ $subfeature_selector ] = $new_declarations; 5050 } 5051 5052 /* 5053 * Remove the subfeature from the block's node now its 5054 * styles will be included under its own selector not the 5055 * block's. 5056 */ 5057 unset( $node[ $feature ][ $subfeature ] ); 5058 } 5059 } 5060 5061 /* 5062 * Now subfeatures have been processed and removed we can process 5063 * feature root selector or simple string selector. 5064 */ 5065 if ( 5066 is_string( $feature_selectors ) || 5067 ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] ) 5068 ) { 5069 $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root']; 5070 5071 /* 5072 * Create temporary node containing only the feature data 5073 * to leverage existing `compute_style_properties` function. 5074 */ 5075 $feature_node = array( $feature => $node[ $feature ] ); 5076 5077 // Generate the style declarations. 5078 $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json ); 5079 5080 /* 5081 * Merge new declarations with any that already exist for 5082 * the feature selector. This may occur when multiple block 5083 * support features use the same custom selector. 5084 */ 5085 if ( isset( $declarations[ $feature_selector ] ) ) { 5086 foreach ( $new_declarations as $new_declaration ) { 5087 $declarations[ $feature_selector ][] = $new_declaration; 5088 } 5089 } else { 5090 $declarations[ $feature_selector ] = $new_declarations; 5091 } 5092 5093 /* 5094 * Remove the feature from the block's node now its styles 5095 * will be included under its own selector not the block's. 5096 */ 5097 unset( $node[ $feature ] ); 5098 } 5099 } 5100 5101 return $declarations; 5102 } 5103 5104 /** 5105 * Replaces CSS variables with their values in place. 5106 * 5107 * @since 6.3.0 5108 * @since 6.5.0 Check for empty style before processing its value. 5109 * 5110 * @param array $styles CSS declarations to convert. 5111 * @param array $values key => value pairs to use for replacement. 5112 * @return array 5113 */ 5114 private static function convert_variables_to_value( $styles, $values ) { 5115 foreach ( $styles as $key => $style ) { 5116 if ( empty( $style ) ) { 5117 continue; 5118 } 5119 5120 if ( is_array( $style ) ) { 5121 $styles[ $key ] = self::convert_variables_to_value( $style, $values ); 5122 continue; 5123 } 5124 5125 if ( 0 <= strpos( $style, 'var(' ) ) { 5126 // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group. 5127 5128 $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts ); 5129 5130 if ( $has_matches ) { 5131 $resolved_style = $styles[ $key ]; 5132 foreach ( $var_parts[1] as $index => $var_part ) { 5133 $key_in_values = 'var(' . $var_part . ')'; 5134 $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan). 5135 $fallback = $var_parts[2][ $index ]; // the fallback value. 5136 $resolved_style = str_replace( 5137 array( 5138 $rule_to_replace, 5139 $fallback, 5140 ), 5141 array( 5142 $values[ $key_in_values ] ?? $rule_to_replace, 5143 $values[ $fallback ] ?? $fallback, 5144 ), 5145 $resolved_style 5146 ); 5147 } 5148 $styles[ $key ] = $resolved_style; 5149 } 5150 } 5151 } 5152 5153 return $styles; 5154 } 5155 5156 /** 5157 * Resolves the values of CSS variables in the given styles. 5158 * 5159 * @since 6.3.0 5160 * 5161 * @param WP_Theme_JSON $theme_json The theme json resolver. 5162 * @return WP_Theme_JSON The $theme_json with resolved variables. 5163 */ 5164 public static function resolve_variables( $theme_json ) { 5165 $settings = $theme_json->get_settings(); 5166 $styles = $theme_json->get_raw_data()['styles']; 5167 $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS ); 5168 $theme_vars = static::compute_theme_vars( $settings ); 5169 $vars = array_reduce( 5170 array_merge( $preset_vars, $theme_vars ), 5171 function ( $carry, $item ) { 5172 $name = $item['name']; 5173 $carry[ "var({$name})" ] = $item['value']; 5174 return $carry; 5175 }, 5176 array() 5177 ); 5178 5179 $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); 5180 return $theme_json; 5181 } 5182 5183 /** 5184 * Generates a selector for a block style variation. 5185 * 5186 * @since 6.5.0 5187 * 5188 * @param string $variation_name Name of the block style variation. 5189 * @param string $block_selector CSS selector for the block. 5190 * @return string Block selector with block style variation selector added to it. 5191 */ 5192 protected static function get_block_style_variation_selector( $variation_name, $block_selector ) { 5193 $variation_class = ".is-style-$variation_name"; 5194 5195 if ( ! $block_selector ) { 5196 return $variation_class; 5197 } 5198 5199 $limit = 1; 5200 $selector_parts = explode( ',', $block_selector ); 5201 $result = array(); 5202 5203 foreach ( $selector_parts as $part ) { 5204 $result[] = preg_replace_callback( 5205 '/((?::\([^)]+\))?\s*)([^\s:]+)/', 5206 function ( $matches ) use ( $variation_class ) { 5207 return $matches[1] . $matches[2] . $variation_class; 5208 }, 5209 $part, 5210 $limit 5211 ); 5212 } 5213 5214 return implode( ',', $result ); 5215 } 5216 5217 /** 5218 * Collects valid block style variations keyed by block type. 5219 * 5220 * @since 6.6.0 5221 * @since 6.8.0 Added the `$blocks_metadata` parameter. 5222 * 5223 * @param array $blocks_metadata Optional. List of metadata per block. Default is the metadata for all blocks. 5224 * @return array Valid block style variations by block type. 5225 */ 5226 protected static function get_valid_block_style_variations( $blocks_metadata = array() ) { 5227 $valid_variations = array(); 5228 $blocks_metadata = empty( $blocks_metadata ) ? static::get_blocks_metadata() : $blocks_metadata; 5229 foreach ( $blocks_metadata as $block_name => $block_meta ) { 5230 if ( ! isset( $block_meta['styleVariations'] ) ) { 5231 continue; 5232 } 5233 $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] ); 5234 } 5235 5236 return $valid_variations; 5237 } 5238 5239 /** 5240 * Extracts the block name from the block metadata path. 5241 * 5242 * @since 7.1.0 5243 * 5244 * @param array $block_metadata Block metadata. 5245 * @return string|null The block name or null if not found. 5246 */ 5247 private static function get_block_name_from_metadata_path( $block_metadata ) { 5248 return $block_metadata['path'][2] ?? null; 5249 } 5250 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Jun 26 08:20:11 2026 | Cross-referenced by PHPXref |