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