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