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