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