PluginProbe
Gutenberg / 17.0.2
Gutenberg v17.0.2
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
← All changes | lib/class-wp-theme-json-gutenberg.php +410 -2571 23.7.0 → 17.0.2 View file →
@@ -5,8 +5,12 @@
5 5 * @package gutenberg
6 6 * @since 5.8.0
7 7 */
8 8
9 +if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) {
10 + return;
11 +}
12 +
9 13 /**
10 14 * Class that encapsulates the processing of structures that adhere to the theme.json spec.
11 15 *
12 16 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
@@ -37,16 +41,8 @@
37 41 */
38 42 protected static $blocks_metadata = array();
39 43
40 44 /**
41 - * The CSS selector for the top-level preset settings.
42 - *
43 - * @since 6.6.0
44 - * @var string
45 - */
46 - const ROOT_CSS_PROPERTIES_SELECTOR = ':root';
47 -
48 - /**
49 45 * The CSS selector for the top-level styles.
50 46 *
51 47 * @since 5.8.0
52 48 * @var string
@@ -122,23 +118,12 @@
122 118 * `use_default_names` preset key, and simplified the metadata structure.
123 119 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
124 120 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 121 * @since 6.2.0 Added 'shadow' presets.
126 - * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 - * @since 6.6.0 Added `aspectRatios`.
128 122 * @var array
129 123 */
130 124 const PRESETS_METADATA = array(
131 125 array(
132 - 'path' => array( 'dimensions', 'aspectRatios' ),
133 - 'prevent_override' => array( 'dimensions', 'defaultAspectRatios' ),
134 - 'use_default_names' => false,
135 - 'value_key' => 'ratio',
136 - 'css_vars' => '--wp--preset--aspect-ratio--$slug',
137 - 'classes' => array(),
138 - 'properties' => array( 'aspect-ratio' ),
139 - ),
140 - array(
141 126 'path' => array( 'color', 'palette' ),
142 127 'prevent_override' => array( 'color', 'defaultPalette' ),
143 128 'use_default_names' => false,
144 129 'value_key' => 'color',
@@ -169,9 +154,9 @@
169 154 'properties' => array( 'filter' ),
170 155 ),
171 156 array(
172 157 'path' => array( 'typography', 'fontSizes' ),
173 - 'prevent_override' => array( 'typography', 'defaultFontSizes' ),
158 + 'prevent_override' => false,
174 159 'use_default_names' => true,
175 160 'value_func' => 'gutenberg_get_typography_font_size_value',
176 161 'css_vars' => '--wp--preset--font-size--$slug',
177 162 'classes' => array( '.has-$slug-font-size' => 'font-size' ),
@@ -187,9 +172,9 @@
187 172 'properties' => array( 'font-family' ),
188 173 ),
189 174 array(
190 175 'path' => array( 'spacing', 'spacingSizes' ),
191 - 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
176 + 'prevent_override' => false,
192 177 'use_default_names' => true,
193 178 'value_key' => 'size',
194 179 'css_vars' => '--wp--preset--spacing--$slug',
195 180 'classes' => array(),
@@ -203,26 +188,8 @@
203 188 'css_vars' => '--wp--preset--shadow--$slug',
204 189 'classes' => array(),
205 190 'properties' => array( 'box-shadow' ),
206 191 ),
207 - array(
208 - 'path' => array( 'border', 'radiusSizes' ),
209 - 'prevent_override' => false,
210 - 'use_default_names' => false,
211 - 'value_key' => 'size',
212 - 'css_vars' => '--wp--preset--border-radius--$slug',
213 - 'classes' => array(),
214 - 'properties' => array( 'border-radius' ),
215 - ),
216 - array(
217 - 'path' => array( 'dimensions', 'dimensionSizes' ),
218 - 'prevent_override' => false,
219 - 'use_default_names' => false,
220 - 'value_key' => 'size',
221 - 'css_vars' => '--wp--preset--dimension--$slug',
222 - 'classes' => array(),
223 - 'properties' => array( 'width', 'height', 'min-height' ),
224 - ),
225 192 );
226 193
227 194 /**
228 195 * Metadata for style properties.
@@ -239,23 +206,14 @@
239 206 * `--wp--style--root--padding-*`, and `box-shadow` properties,
240 207 * removed the `--wp--style--block-gap` property.
241 208 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 209 * @since 6.3.0 Added `writing-mode` property.
243 - * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 - * `typography.textIndent` properties.
246 210 *
247 211 * @var array
248 212 */
249 213 const PROPERTIES_METADATA = array(
250 - 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ),
251 214 'background' => array( 'color', 'gradient' ),
252 215 'background-color' => array( 'color', 'background' ),
253 - 'background-image' => array( 'background', 'backgroundImage' ),
254 - 'background-position' => array( 'background', 'backgroundPosition' ),
255 - 'background-repeat' => array( 'background', 'backgroundRepeat' ),
256 - 'background-size' => array( 'background', 'backgroundSize' ),
257 - 'background-attachment' => array( 'background', 'backgroundAttachment' ),
258 216 'border-radius' => array( 'border', 'radius' ),
259 217 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
260 218 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
261 219 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
@@ -275,9 +233,8 @@
275 233 'border-left-color' => array( 'border', 'left', 'color' ),
276 234 'border-left-width' => array( 'border', 'left', 'width' ),
277 235 'border-left-style' => array( 'border', 'left', 'style' ),
278 236 'color' => array( 'color', 'text' ),
279 - 'text-align' => array( 'typography', 'textAlign' ),
280 237 'column-count' => array( 'typography', 'textColumns' ),
281 238 'font-family' => array( 'typography', 'fontFamily' ),
282 239 'font-size' => array( 'typography', 'fontSize' ),
283 240 'font-style' => array( 'typography', 'fontStyle' ),
@@ -289,9 +246,8 @@
289 246 'margin-right' => array( 'spacing', 'margin', 'right' ),
290 247 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
291 248 'margin-left' => array( 'spacing', 'margin', 'left' ),
292 249 'min-height' => array( 'dimensions', 'minHeight' ),
293 - 'min-width' => array( 'dimensions', 'minWidth' ),
294 250 'outline-color' => array( 'outline', 'color' ),
295 251 'outline-offset' => array( 'outline', 'offset' ),
296 252 'outline-style' => array( 'outline', 'style' ),
297 253 'outline-width' => array( 'outline', 'width' ),
@@ -305,15 +261,11 @@
305 261 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 262 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 263 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 264 'text-decoration' => array( 'typography', 'textDecoration' ),
309 - 'text-shadow' => array( 'typography', 'textShadow' ),
310 265 'text-transform' => array( 'typography', 'textTransform' ),
311 - 'text-indent' => array( 'typography', 'textIndent' ),
312 266 'filter' => array( 'filter', 'duotone' ),
313 267 'box-shadow' => array( 'shadow' ),
314 - 'height' => array( 'dimensions', 'height' ),
315 - 'width' => array( 'dimensions', 'width' ),
316 268 'writing-mode' => array( 'typography', 'writingMode' ),
317 269 );
318 270
319 271 /**
@@ -323,37 +275,45 @@
323 275 * paths to the value in theme.json & block attributes.
324 276 *
325 277 * Indirect properties are not output directly by `compute_style_properties`,
326 278 * but are used elsewhere in the processing of global styles. The indirect
327 - * property is used to validate whether a style value is allowed.
279 + * property is used to validate whether or not a style value is allowed.
328 280 *
329 281 * @since 6.2.0
330 - * @since 6.6.0 Added background-image properties.
331 282 *
332 283 * @var array
333 284 */
334 285 const INDIRECT_PROPERTIES_METADATA = array(
335 - 'gap' => array(
286 + 'gap' => array(
336 287 array( 'spacing', 'blockGap' ),
337 288 ),
338 - 'column-gap' => array(
289 + 'column-gap' => array(
339 290 array( 'spacing', 'blockGap', 'left' ),
340 291 ),
341 - 'row-gap' => array(
292 + 'row-gap' => array(
342 293 array( 'spacing', 'blockGap', 'top' ),
343 294 ),
344 - 'max-width' => array(
295 + 'max-width' => array(
345 296 array( 'layout', 'contentSize' ),
346 297 array( 'layout', 'wideSize' ),
347 298 ),
348 - 'background-image' => array(
349 - array( 'background', 'backgroundImage', 'url' ),
350 - array( 'background', 'gradient' ),
351 - ),
352 299 );
353 300
301 + /**
302 + * Protected style properties.
303 + *
304 + * These style properties are only rendered if a setting enables it
305 + * via a value other than `null`.
306 + *
307 + * Each element maps the style property to the corresponding theme.json
308 + * setting key.
309 + *
310 + * @since 5.9.0
311 + */
312 + const PROTECTED_PROPERTIES = array(
313 + 'spacing.blockGap' => array( 'spacing', 'blockGap' ),
314 + );
354 315
355 -
356 316 /**
357 317 * The top-level keys a theme.json can have.
358 318 *
359 319 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
@@ -362,9 +322,8 @@
362 322 * @since 6.3.0 Added the `description` value.
363 323 * @var string[]
364 324 */
365 325 const VALID_TOP_LEVEL_KEYS = array(
366 - 'blockTypes',
367 326 'customTemplates',
368 327 'description',
369 328 'patterns',
370 329 'settings',
@@ -370,9 +329,8 @@
370 329 'settings',
371 330 'styles',
372 331 'templateParts',
373 332 'title',
374 - 'slug',
375 333 'version',
376 334 );
377 335
378 336 /**
@@ -388,12 +346,8 @@
388 346 * `position.fixed` and `position.sticky`.
389 347 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
390 348 * @since 6.4.0 Added `layout.allowEditing`.
391 349 * @since 6.4.0 Added `lightbox`.
392 - * @since 7.0.0 Added type markers to the schema for boolean values.
393 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
394 - * `typography.textIndent` properties.
395 - * @since 7.1.0 Added `viewport` property.
396 350 * @var array
397 351 */
398 352 const VALID_SETTINGS = array(
399 353 'appearanceTools' => null,
@@ -399,20 +353,14 @@
399 353 'appearanceTools' => null,
400 354 'useRootPaddingAwareAlignments' => null,
401 355 'background' => array(
402 356 'backgroundImage' => null,
403 - 'backgroundSize' => null,
404 - 'gradient' => null,
405 357 ),
406 - 'blockVisibility' => array(
407 - 'allowEditing' => true,
408 - ),
409 358 'border' => array(
410 - 'color' => null,
411 - 'radius' => null,
412 - 'style' => null,
413 - 'width' => null,
414 - 'radiusSizes' => null,
359 + 'color' => null,
360 + 'radius' => null,
361 + 'style' => null,
362 + 'width' => null,
415 363 ),
416 364 'color' => array(
417 365 'background' => null,
418 366 'custom' => null,
@@ -431,26 +379,18 @@
431 379 'text' => null,
432 380 ),
433 381 'custom' => null,
434 382 'dimensions' => array(
435 - 'aspectRatio' => null,
436 - 'aspectRatios' => null,
437 - 'defaultAspectRatios' => null,
438 - 'dimensionSizes' => null,
439 - 'height' => null,
440 - 'minHeight' => null,
441 - 'minWidth' => null,
442 - 'width' => null,
383 + 'minHeight' => null,
443 384 ),
444 385 'layout' => array(
445 - 'contentSize' => null,
446 - 'wideSize' => null,
447 - 'allowEditing' => null,
448 - 'allowCustomContentAndWideSize' => null,
386 + 'contentSize' => null,
387 + 'wideSize' => null,
388 + 'allowEditing' => null,
449 389 ),
450 390 'lightbox' => array(
451 - 'enabled' => true,
452 - 'allowEditing' => true,
391 + 'enabled' => null,
392 + 'allowEditing' => null,
453 393 ),
454 394 'position' => array(
455 395 'fixed' => null,
456 396 'sticky' => null,
@@ -455,16 +395,15 @@
455 395 'fixed' => null,
456 396 'sticky' => null,
457 397 ),
458 398 'spacing' => array(
459 - 'customSpacingSize' => null,
460 - 'defaultSpacingSizes' => null,
461 - 'spacingSizes' => null,
462 - 'spacingScale' => null,
463 - 'blockGap' => null,
464 - 'margin' => null,
465 - 'padding' => null,
466 - 'units' => null,
399 + 'customSpacingSize' => null,
400 + 'spacingSizes' => null,
401 + 'spacingScale' => null,
402 + 'blockGap' => null,
403 + 'margin' => null,
404 + 'padding' => null,
405 + 'units' => null,
467 406 ),
468 407 'shadow' => array(
469 408 'presets' => null,
470 409 'defaultPresets' => null,
@@ -469,56 +408,24 @@
469 408 'presets' => null,
470 409 'defaultPresets' => null,
471 410 ),
472 411 'typography' => array(
473 - 'fluid' => null,
474 - 'customFontSize' => null,
475 - 'defaultFontSizes' => null,
476 - 'dropCap' => null,
477 - 'fontFamilies' => null,
478 - 'fontSizes' => null,
479 - 'fontStyle' => null,
480 - 'fontWeight' => null,
481 - 'letterSpacing' => null,
482 - 'lineHeight' => null,
483 - 'textAlign' => null,
484 - 'textColumns' => null,
485 - 'textDecoration' => null,
486 - 'textIndent' => null,
487 - 'textTransform' => null,
488 - 'writingMode' => null,
412 + 'fluid' => null,
413 + 'customFontSize' => null,
414 + 'dropCap' => null,
415 + 'fontFamilies' => null,
416 + 'fontSizes' => null,
417 + 'fontStyle' => null,
418 + 'fontWeight' => null,
419 + 'letterSpacing' => null,
420 + 'lineHeight' => null,
421 + 'textColumns' => null,
422 + 'textDecoration' => null,
423 + 'textTransform' => null,
424 + 'writingMode' => null,
489 425 ),
490 - 'viewport' => array(
491 - 'mobile' => null,
492 - 'tablet' => null,
493 - ),
494 426 );
495 427
496 - const FONT_FAMILY_SCHEMA = array(
497 - array(
498 - 'fontFamily' => null,
499 - 'name' => null,
500 - 'slug' => null,
501 - 'fontFace' => array(
502 - array(
503 - 'ascentOverride' => null,
504 - 'descentOverride' => null,
505 - 'fontDisplay' => null,
506 - 'fontFamily' => null,
507 - 'fontFeatureSettings' => null,
508 - 'fontStyle' => null,
509 - 'fontStretch' => null,
510 - 'fontVariationSettings' => null,
511 - 'fontWeight' => null,
512 - 'lineGapOverride' => null,
513 - 'sizeAdjust' => null,
514 - 'src' => null,
515 - 'unicodeRange' => null,
516 - ),
517 - ),
518 - ),
519 - );
520 -
521 428 /**
522 429 * The valid properties under the styles key.
523 430 *
524 431 * @since 5.8.0 As `ALLOWED_STYLES`.
@@ -528,23 +435,12 @@
528 435 * @since 6.1.0 Added new side properties for `border`,
529 436 * added new property `shadow`,
530 437 * updated `blockGap` to be allowed at any level.
531 438 * @since 6.2.0 Added `outline`, and `minHeight` properties.
532 - * @since 6.6.0 Added `background` sub properties to top-level only.
533 - * @since 6.6.0 Added `dimensions.aspectRatio`.
534 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
535 - * `typography.textIndent` properties.
439 + *
536 440 * @var array
537 441 */
538 442 const VALID_STYLES = array(
539 - 'background' => array(
540 - 'backgroundImage' => null,
541 - 'backgroundAttachment' => null,
542 - 'backgroundPosition' => null,
543 - 'backgroundRepeat' => null,
544 - 'backgroundSize' => null,
545 - 'gradient' => null,
546 - ),
547 443 'border' => array(
548 444 'color' => null,
549 445 'radius' => null,
550 446 'style' => null,
@@ -559,13 +455,9 @@
559 455 'gradient' => null,
560 456 'text' => null,
561 457 ),
562 458 'dimensions' => array(
563 - 'aspectRatio' => null,
564 - 'height' => null,
565 - 'minHeight' => null,
566 - 'minWidth' => null,
567 - 'width' => null,
459 + 'minHeight' => null,
568 460 ),
569 461 'filter' => array(
570 462 'duotone' => null,
571 463 ),
@@ -587,13 +479,10 @@
587 479 'fontStyle' => null,
588 480 'fontWeight' => null,
589 481 'letterSpacing' => null,
590 482 'lineHeight' => null,
591 - 'textAlign' => null,
592 483 'textColumns' => null,
593 484 'textDecoration' => null,
594 - 'textIndent' => null,
595 - 'textShadow' => null,
596 485 'textTransform' => null,
597 486 'writingMode' => null,
598 487 ),
599 488 'css' => null,
@@ -601,9 +490,9 @@
601 490
602 491 /**
603 492 * Defines which pseudo selectors are enabled for which elements.
604 493 *
605 - * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
494 + * The order of the selectors should be: link, any-link, visited, hover, focus, active.
606 495 * This is to ensure the user action (hover, focus and active) styles have a higher
607 496 * specificity than the visited styles, which in turn have a higher specificity than
608 497 * the unvisited styles.
609 498 *
@@ -611,219 +500,15 @@
611 500 * Note: this will affect both top-level and block-level elements.
612 501 *
613 502 * @since 6.1.0
614 503 * @since 6.2.0 Added support for `:link` and `:any-link`.
615 - * @since 6.8.0 Added support for `:focus-visible`.
616 504 */
617 505 const VALID_ELEMENT_PSEUDO_SELECTORS = array(
618 - 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
619 - 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
506 + 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
507 + 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
620 508 );
621 509
622 510 /**
623 - * The valid pseudo-selectors that can be used for blocks.
624 - *
625 - * @since 7.0.0
626 - * @var array
627 - */
628 - const VALID_BLOCK_PSEUDO_SELECTORS = array(
629 - 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
630 - 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
631 - );
632 -
633 - /**
634 - * Default viewport breakpoint sizes.
635 - *
636 - * @since 7.1.0
637 - * @var array
638 - */
639 - const DEFAULT_VIEWPORT_BREAKPOINTS = array(
640 - 'mobile' => '480px',
641 - 'tablet' => '782px',
642 - );
643 -
644 - /**
645 - * Returns CSS media queries for responsive viewport style states.
646 - *
647 - * Breakpoint values are read from `settings.viewport`, sanitized, and
648 - * normalized before the media query strings are generated. By default, the
649 - * returned keys are the theme.json style-state names (`@mobile`, `@tablet`).
650 - * When `$options['include_desktop']` is truthy, `@desktop` is included.
651 - *
652 - * @since 7.1.0
653 - *
654 - * @param mixed $viewport_settings Viewport settings from theme.json.
655 - * @param array $options {
656 - * Optional. Options for generating media queries.
657 - *
658 - * @type bool $include_desktop Whether to include the desktop media query. Default false.
659 - * }
660 - * @return array Responsive media queries.
661 - */
662 - public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) {
663 - $breakpoints = static::sanitize_viewport_settings( $viewport_settings );
664 -
665 - $responsive_media_queries = array();
666 -
667 - if ( isset( $breakpoints['mobile'] ) ) {
668 - $responsive_media_queries['@mobile'] = "@media (width <= {$breakpoints['mobile']})";
669 - }
670 -
671 - if ( isset( $breakpoints['tablet'] ) ) {
672 - $responsive_media_queries['@tablet'] = isset( $breakpoints['mobile'] )
673 - ? sprintf(
674 - '@media (%s < width <= %s)',
675 - $breakpoints['mobile'],
676 - $breakpoints['tablet']
677 - )
678 - : "@media (width <= {$breakpoints['tablet']})";
679 - }
680 -
681 - if ( ! empty( $options['include_desktop'] ) ) {
682 - if ( isset( $breakpoints['tablet'] ) ) {
683 - $desktop_breakpoint = $breakpoints['tablet'];
684 - } else {
685 - $desktop_breakpoint = $breakpoints['mobile'];
686 - }
687 -
688 - $responsive_media_queries['@desktop'] =
689 - "@media (width > {$desktop_breakpoint})";
690 - }
691 -
692 - return $responsive_media_queries;
693 - }
694 -
695 - /**
696 - * Checks whether a viewport breakpoint value is a safe CSS length.
697 - *
698 - * Viewport breakpoints are limited to numeric `px`, `em`, and `rem` lengths.
699 - * CSS functions, percentages, and other units are rejected because breakpoint
700 - * values are interpolated into generated media queries.
701 - *
702 - * @since 7.1.0
703 - *
704 - * @param mixed $value Value to check.
705 - * @return bool Whether the value is valid.
706 - */
707 - private static function is_valid_viewport_breakpoint_size( $value ) {
708 - if ( ! is_string( $value ) ) {
709 - return false;
710 - }
711 -
712 - $value = trim( $value );
713 - if ( '' === $value ) {
714 - return false;
715 - }
716 -
717 - return 1 === preg_match( '/^(?:\d+|\d*\.\d+)(?:px|em|rem)$/', $value );
718 - }
719 -
720 - /**
721 - * Converts a valid viewport breakpoint size to pixels for ordering checks.
722 - *
723 - * Generated media queries keep the original units. This method only
724 - * normalizes values so `mobile` and `tablet` can be compared safely. `em`
725 - * and `rem` lengths use a 16px base for comparison.
726 - *
727 - * @since 7.1.0
728 - *
729 - * @param mixed $value Viewport breakpoint size.
730 - * @return float|null Viewport breakpoint size in pixels, or null when invalid.
731 - */
732 - private static function get_viewport_breakpoint_value_in_pixels( $value ) {
733 - if ( ! static::is_valid_viewport_breakpoint_size( $value ) ) {
734 - return null;
735 - }
736 -
737 - $value = trim( $value );
738 - $unit = substr( $value, -3 );
739 - if ( 'rem' === $unit ) {
740 - $number = (float) substr( $value, 0, -3 );
741 - } else {
742 - $unit = substr( $value, -2 );
743 - $number = (float) substr( $value, 0, -2 );
744 - }
745 -
746 - /*
747 - * Use the most common browser default font size as the base for em/rem
748 - * media query conversions. This pixel value is only used to compare
749 - * breakpoint order; generated media queries keep the original units.
750 - */
751 - return 'px' === $unit ? $number : $number * 16;
752 - }
753 -
754 - /**
755 - * Sanitizes and normalizes viewport breakpoint settings.
756 - *
757 - * Keeps only supported breakpoint keys, trims valid CSS lengths, and returns
758 - * the default breakpoints when no valid custom breakpoint is provided. When
759 - * only one breakpoint is valid, it remains keyed by its configured state and
760 - * uses a single max-width media query. When `tablet` is not larger than
761 - * `mobile`, it is removed.
762 - *
763 - * @since 7.1.0
764 - *
765 - * @param mixed $viewport_settings Viewport settings from theme.json.
766 - * @return array Sanitized viewport breakpoint settings.
767 - */
768 - private static function sanitize_viewport_settings( $viewport_settings ) {
769 - if ( ! is_array( $viewport_settings ) ) {
770 - return static::DEFAULT_VIEWPORT_BREAKPOINTS;
771 - }
772 -
773 - $breakpoints = array();
774 - foreach ( array_keys( static::DEFAULT_VIEWPORT_BREAKPOINTS ) as $breakpoint ) {
775 - $value = $viewport_settings[ $breakpoint ] ?? null;
776 - $px = static::get_viewport_breakpoint_value_in_pixels( $value );
777 - if ( null !== $px ) {
778 - $breakpoints[ $breakpoint ] = array(
779 - 'value' => trim( $value ),
780 - 'px' => $px,
781 - );
782 - }
783 - }
784 -
785 - if ( empty( $breakpoints ) ) {
786 - return static::DEFAULT_VIEWPORT_BREAKPOINTS;
787 - }
788 -
789 - if ( 1 === count( $breakpoints ) ) {
790 - $breakpoint = key( $breakpoints );
791 - return array( $breakpoint => $breakpoints[ $breakpoint ]['value'] );
792 - }
793 -
794 - $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] );
795 -
796 - if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px']
797 - ) {
798 - $sanitized['tablet'] = $breakpoints['tablet']['value'];
799 - }
800 -
801 - return $sanitized;
802 - }
803 -
804 - /**
805 - * Custom states for blocks that map to CSS class selectors rather than
806 - * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
807 - * distinguish them from real CSS pseudo-selectors and breakpoint states.
808 - *
809 - * The CSS selector for each state is defined in the block's block.json
810 - * under `selectors.states`, e.g.:
811 - *
812 - * "selectors": { "states": { "-current": ".some-css-selector" } }
813 - *
814 - * This constant controls which states are valid in theme.json for a given
815 - * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
816 - * as valid sub-states, producing compound selectors such as
817 - * `.wp-block-navigation-item.current-menu-item:hover`.
818 - *
819 - * @var array
820 - */
821 - const VALID_BLOCK_CUSTOM_STATES = array(
822 - 'core/navigation-link' => array( '-current' ),
823 - );
824 -
825 - /**
826 511 * The valid elements that can be found under styles.
827 512 *
828 513 * @since 5.8.0
829 514 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
@@ -829,23 +514,21 @@
829 514 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
830 515 * @var string[]
831 516 */
832 517 const ELEMENTS = array(
833 - 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
834 - 'heading' => 'h1, h2, h3, h4, h5, h6',
835 - 'h1' => 'h1',
836 - 'h2' => 'h2',
837 - 'h3' => 'h3',
838 - 'h4' => 'h4',
839 - 'h5' => 'h5',
840 - 'h6' => 'h6',
518 + 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
519 + 'heading' => 'h1, h2, h3, h4, h5, h6',
520 + 'h1' => 'h1',
521 + 'h2' => 'h2',
522 + 'h3' => 'h3',
523 + 'h4' => 'h4',
524 + 'h5' => 'h5',
525 + 'h6' => 'h6',
841 526 // We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
842 - 'button' => '.wp-element-button, .wp-block-button__link',
527 + 'button' => '.wp-element-button, .wp-block-button__link',
843 528 // The block classes are necessary to target older content that won't use the new class names.
844 - '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',
845 - 'cite' => 'cite',
846 - 'select' => 'select',
847 - 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
529 + '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',
530 + 'cite' => 'cite',
848 531 );
849 532
850 533 const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
851 534 'button' => 'wp-element-button',
@@ -861,120 +544,13 @@
861 544 */
862 545 const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
863 546 '__experimentalBorder' => 'border',
864 547 'color' => 'color',
865 - 'dimensions' => 'dimensions',
866 548 'spacing' => 'spacing',
867 549 'typography' => 'typography',
868 550 );
869 551
870 552 /**
871 - * Return the input schema at the root and per origin.
872 - *
873 - * @since 6.5.0
874 - *
875 - * @param array $schema The base schema.
876 - * @return array The schema at the root and per origin.
877 - *
878 - * Example:
879 - * schema_in_root_and_per_origin(
880 - * array(
881 - * 'fontFamily' => null,
882 - * 'slug' => null,
883 - * )
884 - * )
885 - *
886 - * Returns:
887 - * array(
888 - * 'fontFamily' => null,
889 - * 'slug' => null,
890 - * 'default' => array(
891 - * 'fontFamily' => null,
892 - * 'slug' => null,
893 - * ),
894 - * 'blocks' => array(
895 - * 'fontFamily' => null,
896 - * 'slug' => null,
897 - * ),
898 - * 'theme' => array(
899 - * 'fontFamily' => null,
900 - * 'slug' => null,
901 - * ),
902 - * 'custom' => array(
903 - * 'fontFamily' => null,
904 - * 'slug' => null,
905 - * ),
906 - * )
907 - */
908 - protected static function schema_in_root_and_per_origin( $schema ) {
909 - $schema_in_root_and_per_origin = $schema;
910 - foreach ( static::VALID_ORIGINS as $origin ) {
911 - $schema_in_root_and_per_origin[ $origin ] = $schema;
912 - }
913 - return $schema_in_root_and_per_origin;
914 - }
915 -
916 - /**
917 - * Processes pseudo-selectors for any node (block or variation).
918 - *
919 - * @param array $node The node data (block or variation).
920 - * @param string $base_selector The base selector.
921 - * @param array $settings The theme settings.
922 - * @param string $block_name The block name.
923 - * @param array|null $block_metadata Metadata about the block to get styles for.
924 - * @param array|null $style_variation Style variation metadata.
925 - * @return array Array of pseudo-selector declarations.
926 - */
927 - private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
928 - $pseudo_declarations = array();
929 - $add_declarations = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
930 - if ( empty( $declarations ) ) {
931 - return;
932 - }
933 -
934 - if ( isset( $pseudo_declarations[ $selector ] ) ) {
935 - $pseudo_declarations[ $selector ] = array_merge(
936 - $pseudo_declarations[ $selector ],
937 - $declarations
938 - );
939 - } else {
940 - $pseudo_declarations[ $selector ] = $declarations;
941 - }
942 - };
943 -
944 - if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
945 - return $pseudo_declarations;
946 - }
947 -
948 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
949 - if ( isset( $node[ $pseudo_selector ] ) ) {
950 - $pseudo_node = $node[ $pseudo_selector ];
951 -
952 - if ( is_array( $block_metadata ) ) {
953 - $feature_declarations = $this->get_feature_declarations_for_node( $block_metadata, $pseudo_node );
954 - $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
955 - $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
956 -
957 - foreach ( $feature_declarations as $feature_selector => $declarations ) {
958 - $target_selector = is_array( $style_variation )
959 - ? static::get_block_style_variation_feature_selector( $style_variation, $feature_selector )
960 - : $feature_selector;
961 - $combined_selector = static::append_to_selector( $target_selector, $pseudo_selector );
962 -
963 - $add_declarations( $combined_selector, $declarations );
964 - }
965 - }
966 -
967 - $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
968 - $declarations = static::compute_style_properties( $pseudo_node, $settings, null, null );
969 - $add_declarations( $combined_selector, $declarations );
970 - }
971 - }
972 -
973 - return $pseudo_declarations;
974 - }
975 -
976 - /**
977 553 * Returns a class name by an element name.
978 554 *
979 555 * @since 6.1.0
980 556 *
@@ -995,15 +571,12 @@
995 571 * Options that settings.appearanceTools enables.
996 572 *
997 573 * @since 6.0.0
998 574 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
999 - * @since 7.0.0 Added `dimensions.width` and `dimensions.height`.
1000 575 * @var array
1001 576 */
1002 577 const APPEARANCE_TOOLS_OPT_INS = array(
1003 578 array( 'background', 'backgroundImage' ),
1004 - array( 'background', 'backgroundSize' ),
1005 - array( 'background', 'gradient' ),
1006 579 array( 'border', 'color' ),
1007 580 array( 'border', 'radius' ),
1008 581 array( 'border', 'style' ),
1009 582 array( 'border', 'width' ),
@@ -1010,13 +583,9 @@
1010 583 array( 'color', 'link' ),
1011 584 array( 'color', 'heading' ),
1012 585 array( 'color', 'button' ),
1013 586 array( 'color', 'caption' ),
1014 - array( 'dimensions', 'aspectRatio' ),
1015 - array( 'dimensions', 'height' ),
1016 587 array( 'dimensions', 'minHeight' ),
1017 - array( 'dimensions', 'minWidth' ),
1018 - array( 'dimensions', 'width' ),
1019 588 // BEGIN EXPERIMENTAL.
1020 589 // Allow `position.fixed` to be opted-in by default.
1021 590 // Sticky position support was backported to WordPress 6.2 in https://core.trac.wordpress.org/ticket/57618.
1022 591 // While `fixed` was included as a valid setting, exposing it by default is still experimental.
@@ -1026,9 +595,8 @@
1026 595 array( 'spacing', 'blockGap' ),
1027 596 array( 'spacing', 'margin' ),
1028 597 array( 'spacing', 'padding' ),
1029 598 array( 'typography', 'lineHeight' ),
1030 - array( 'typography', 'textColumns' ),
1031 599 );
1032 600
1033 601 /**
1034 602 * The latest version of the schema in use.
@@ -1034,40 +602,39 @@
1034 602 * The latest version of the schema in use.
1035 603 *
1036 604 * @since 5.8.0
1037 605 * @since 5.9.0 Changed value from 1 to 2.
1038 - * @since 6.5.0 Changed value from 2 to 3.
1039 606 * @var int
1040 607 */
1041 - const LATEST_SCHEMA = 3;
608 + const LATEST_SCHEMA = 2;
1042 609
1043 610 /**
1044 611 * Constructor.
1045 612 *
1046 613 * @since 5.8.0
1047 - * @since 6.6.0 Key spacingScale by origin, and pre-generate the spacingSizes from spacingScale.
1048 - * Added unwrapping of shared block style variations into block type variations if registered.
1049 614 *
1050 615 * @param array $theme_json A structure that follows the theme.json schema.
1051 616 * @param string $origin Optional. What source of data this object represents.
1052 - * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
617 + * One of 'default', 'theme', or 'custom'. Default 'theme'.
1053 618 */
1054 - public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA ), $origin = 'theme' ) {
619 + public function __construct( $theme_json = array(), $origin = 'theme' ) {
1055 620 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
1056 621 $origin = 'theme';
1057 622 }
1058 623
1059 - $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
1060 - if ( isset( $this->theme_json['styles'] ) ) {
1061 - $this->theme_json['styles'] = gutenberg_resolve_style_state_aliases( $this->theme_json['styles'] );
624 + $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json );
625 + $registry = WP_Block_Type_Registry::get_instance();
626 + $valid_block_names = array_keys( $registry->get_all_registered() );
627 + $valid_element_names = array_keys( static::ELEMENTS );
628 + $valid_variations = array();
629 + foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) {
630 + if ( ! isset( $block_meta['styleVariations'] ) ) {
631 + continue;
632 + }
633 + $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
1062 634 }
1063 - $blocks_metadata = static::get_blocks_metadata();
1064 - $valid_block_names = array_keys( $blocks_metadata );
1065 - $valid_element_names = array_keys( static::ELEMENTS );
1066 - $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
1067 - $this->theme_json = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
1068 - $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
1069 - $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json );
635 + $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
636 + $this->theme_json = static::maybe_opt_in_into_settings( $theme_json );
1070 637
1071 638 // Internally, presets are keyed by origin.
1072 639 $nodes = static::get_setting_nodes( $this->theme_json );
1073 640 foreach ( $nodes as $node ) {
@@ -1084,100 +651,11 @@
1084 651 }
1085 652 }
1086 653 }
1087 654 }
1088 -
1089 - // In addition to presets, spacingScale (which generates presets) is also keyed by origin.
1090 - $scale_path = array( 'settings', 'spacing', 'spacingScale' );
1091 - $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
1092 - if ( null !== $spacing_scale ) {
1093 - // If the spacingScale is not already keyed by origin.
1094 - if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
1095 - _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
1096 - }
1097 - }
1098 -
1099 - // Pre-generate the spacingSizes from spacingScale.
1100 - $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
1101 - $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
1102 - if ( isset( $spacing_scale ) ) {
1103 - $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
1104 - $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() );
1105 - $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale );
1106 - $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
1107 - _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
1108 - }
1109 655 }
1110 656
1111 657 /**
1112 - * Unwraps shared block style variations.
1113 - *
1114 - * It takes the shared variations (styles.variations.variationName) and
1115 - * applies them to all the blocks that have the given variation registered
1116 - * (styles.blocks.blockType.variations.variationName).
1117 - *
1118 - * For example, given the `core/paragraph` and `core/group` blocks have
1119 - * registered the `section-a` style variation, and given the following input:
1120 - *
1121 - * {
1122 - * "styles": {
1123 - * "variations": {
1124 - * "section-a": { "color": { "background": "backgroundColor" } }
1125 - * }
1126 - * }
1127 - * }
1128 - *
1129 - * It returns the following output:
1130 - *
1131 - * {
1132 - * "styles": {
1133 - * "blocks": {
1134 - * "core/paragraph": {
1135 - * "variations": {
1136 - * "section-a": { "color": { "background": "backgroundColor" } }
1137 - * },
1138 - * },
1139 - * "core/group": {
1140 - * "variations": {
1141 - * "section-a": { "color": { "background": "backgroundColor" } }
1142 - * }
1143 - * }
1144 - * }
1145 - * }
1146 - * }
1147 - *
1148 - * @since 6.6.0
1149 - *
1150 - * @param array $theme_json A structure that follows the theme.json schema.
1151 - * @param array $valid_variations Valid block style variations.
1152 - *
1153 - * @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
1154 - */
1155 - private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
1156 - if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
1157 - return $theme_json;
1158 - }
1159 -
1160 - $new_theme_json = $theme_json;
1161 - $variations = $new_theme_json['styles']['variations'];
1162 -
1163 - foreach ( $valid_variations as $block_type => $registered_variations ) {
1164 - foreach ( $registered_variations as $variation_name ) {
1165 - $block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
1166 - $top_level_data = $variations[ $variation_name ] ?? array();
1167 - $merged_data = array_replace_recursive( $top_level_data, $block_level_data );
1168 - if ( ! empty( $merged_data ) ) {
1169 - _wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
1170 - }
1171 - }
1172 - }
1173 -
1174 - unset( $new_theme_json['styles']['variations'] );
1175 -
1176 - return $new_theme_json;
1177 - }
1178 -
1179 - /**
1180 658 * Enables some opt-in settings if theme declared support.
1181 659 *
1182 660 * @since 5.9.0
1183 661 *
@@ -1228,9 +706,8 @@
1228 706 * Sanitizes the input according to the schemas.
1229 707 *
1230 708 * @since 5.8.0
1231 709 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1232 - * @since 6.6.0 Extended schema definition to allow enhanced block style variations.
1233 710 *
1234 711 * @param array $input Structure to sanitize.
1235 712 * @param array $valid_block_names List of valid block names.
1236 713 * @param array $valid_element_names List of valid element names.
@@ -1265,19 +742,17 @@
1265 742 }
1266 743 }
1267 744
1268 745 // Build the schema based on valid block & element names.
1269 - $schema = array();
1270 - $schema_styles_elements = array();
1271 - $responsive_media_queries = static::get_viewport_media_queries( $input['settings']['viewport'] ?? null );
746 + $schema = array();
747 + $schema_styles_elements = array();
1272 748
1273 749 /*
1274 - * Set allowed element pseudo selectors and responsive breakpoint states.
750 + * Set allowed element pseudo selectors based on per element allow list.
1275 751 * Target data structure in schema:
1276 752 * e.g.
1277 753 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1278 754 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1279 - * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1280 755 */
1281 756 foreach ( $valid_element_names as $element ) {
1282 757 $schema_styles_elements[ $element ] = $styles_non_top_level;
1283 758
@@ -1285,76 +760,13 @@
1285 760 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
1286 761 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
1287 762 }
1288 763 }
1289 -
1290 - // Add responsive breakpoint states for elements.
1291 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1292 - $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1293 - }
1294 764 }
1295 765
1296 766 $schema_styles_blocks = array();
1297 767 $schema_settings_blocks = array();
1298 -
1299 - /*
1300 - * Generate a schema for blocks.
1301 - * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1302 - * - Variations definitions cannot be nested.
1303 - * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states.
1304 - * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1305 - *
1306 - * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1307 - * for further nested inner `blocks`, the overall schema is generated in multiple passes.
1308 - */
1309 768 foreach ( $valid_block_names as $block ) {
1310 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1311 - // `viewport` and `blockVisibility` are global-only settings and cannot be set per block for now.
1312 - unset( $schema_settings_blocks[ $block ]['viewport'] );
1313 - unset( $schema_settings_blocks[ $block ]['blockVisibility'] );
1314 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1315 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1316 -
1317 - // Add responsive breakpoint states for all blocks.
1318 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1319 - $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level;
1320 - $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1321 -
1322 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1323 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1324 - $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1325 - }
1326 - }
1327 - }
1328 -
1329 - // Add pseudo-selectors for blocks that support them.
1330 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1331 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1332 - $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1333 - }
1334 - }
1335 -
1336 - // Add custom states for blocks that support them (e.g. '-current' for navigation).
1337 - if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1338 - foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1339 - $custom_state_schema = $styles_non_top_level;
1340 - // The same pseudo-selectors valid for the block at the top level
1341 - // are also valid within each custom state.
1342 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1343 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) {
1344 - $custom_state_schema[ $pseudo ] = $styles_non_top_level;
1345 - }
1346 - }
1347 - $schema_styles_blocks[ $block ][ $custom_state ] = $custom_state_schema;
1348 - }
1349 - }
1350 - }
1351 -
1352 - $block_style_variation_styles = static::VALID_STYLES;
1353 - $block_style_variation_styles['blocks'] = $schema_styles_blocks;
1354 - $block_style_variation_styles['elements'] = $schema_styles_elements;
1355 -
1356 - foreach ( $valid_block_names as $block ) {
1357 769 // Build the schema for each block style variation.
1358 770 $style_variation_names = array();
1359 771 if (
1360 772 ! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
@@ -1368,44 +780,22 @@
1368 780 }
1369 781
1370 782 $schema_styles_variations = array();
1371 783 if ( ! empty( $style_variation_names ) ) {
1372 - foreach ( $style_variation_names as $variation_name ) {
1373 - $variation_schema = $block_style_variation_styles;
1374 -
1375 - // Add responsive breakpoint states to block style variations.
1376 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1377 - $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1378 - $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1379 - $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks;
1380 -
1381 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1382 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1383 - $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1384 - }
1385 - }
1386 - }
1387 -
1388 - // Add pseudo-selectors to variations for blocks that support them.
1389 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1390 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1391 - $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
1392 - }
1393 - }
1394 -
1395 - $schema_styles_variations[ $variation_name ] = $variation_schema;
1396 - }
784 + $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level );
1397 785 }
1398 786
787 + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
788 + $schema_styles_blocks[ $block ] = $styles_non_top_level;
789 + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1399 790 $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
1400 791 }
1401 792
1402 - $schema['styles'] = static::VALID_STYLES;
1403 - $schema['styles']['blocks'] = $schema_styles_blocks;
1404 - $schema['styles']['elements'] = $schema_styles_elements;
1405 - $schema['settings'] = static::VALID_SETTINGS;
1406 - $schema['settings']['blocks'] = $schema_settings_blocks;
1407 - $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
793 + $schema['styles'] = static::VALID_STYLES;
794 + $schema['styles']['blocks'] = $schema_styles_blocks;
795 + $schema['styles']['elements'] = $schema_styles_elements;
796 + $schema['settings'] = static::VALID_SETTINGS;
797 + $schema['settings']['blocks'] = $schema_settings_blocks;
1408 798
1409 799 // Remove anything that's not present in the schema.
1410 800 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1411 801 if ( ! isset( $input[ $subtree ] ) ) {
@@ -1418,12 +808,8 @@
1418 808 }
1419 809
1420 810 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1421 811
1422 - if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1423 - $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1424 - }
1425 -
1426 812 if ( empty( $result ) ) {
1427 813 unset( $output[ $subtree ] );
1428 814 } else {
1429 815 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1451,38 +837,14 @@
1451 837 protected static function append_to_selector( $selector, $to_append ) {
1452 838 if ( ! str_contains( $selector, ',' ) ) {
1453 839 return $selector . $to_append;
1454 840 }
1455 -
1456 - /**
1457 - * Check for an opportunity to skip the more-costly selector splitting.
1458 - * This should be possible if there are no comments, strings, functions,
1459 - * URLs, escapes, or comment declaration openers (CDOs).
1460 - *
1461 - * Note that this means the fast-path will not apply for selectors like
1462 - * the following incomplete list:
1463 - *
1464 - * - `[class ~= "wide"]`
1465 - * - `.wp-block:is(.is-style-a, .is-style-b)`
1466 - * - `:nth-child(1)`
1467 - *
1468 - * These syntax forms all present opportunities where a comma may not
1469 - * separate selectors. If none of the start characters are present,
1470 - * there should be no way for a comma to mean anything other than a
1471 - * comma token. The exception are syntax errors, which are not handled here.
1472 - *
1473 - * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1474 - */
1475 - if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1476 - return str_replace( ',', $to_append . ',', $selector ) . $to_append;
1477 - }
1478 -
1479 841 $new_selectors = array();
1480 - $selectors = static::split_selector_list( $selector );
842 + $selectors = explode( ',', $selector );
1481 843 foreach ( $selectors as $sel ) {
1482 844 $new_selectors[] = $sel . $to_append;
1483 845 }
1484 - return implode( ', ', $new_selectors );
846 + return implode( ',', $new_selectors );
1485 847 }
1486 848
1487 849 /**
1488 850 * Prepends a sub-selector to an existing one.
@@ -1500,199 +862,17 @@
1500 862 protected static function prepend_to_selector( $selector, $to_prepend ) {
1501 863 if ( ! str_contains( $selector, ',' ) ) {
1502 864 return $to_prepend . $selector;
1503 865 }
1504 -
1505 - /**
1506 - * Check for an opportunity to skip the more-costly selector splitting.
1507 - * This should be possible if there are no comments, strings, functions,
1508 - * URLs, escapes, or comment declaration openers (CDOs).
1509 - *
1510 - * Note that this means the fast-path will not apply for selectors like
1511 - * the following incomplete list:
1512 - *
1513 - * - `[class ~= "wide"]`
1514 - * - `.wp-block:is(.is-style-a, .is-style-b)`
1515 - * - `:nth-child(1)`
1516 - *
1517 - * These syntax forms all present opportunities where a comma may not
1518 - * separate selectors. If none of the start characters are present,
1519 - * there should be no way for a comma to mean anything other than a
1520 - * comma token. The exception are syntax errors, which are not handled here.
1521 - *
1522 - * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1523 - */
1524 - if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1525 - return $to_prepend . str_replace( ',', ',' . $to_prepend, $selector );
1526 - }
1527 -
1528 866 $new_selectors = array();
1529 - $selectors = static::split_selector_list( $selector );
867 + $selectors = explode( ',', $selector );
1530 868 foreach ( $selectors as $sel ) {
1531 869 $new_selectors[] = $to_prepend . $sel;
1532 870 }
1533 -
1534 - return implode( ', ', $new_selectors );
871 + return implode( ',', $new_selectors );
1535 872 }
1536 873
1537 874 /**
1538 - * Splits a selector list into separate selectors.
1539 - *
1540 - * While selectors are joined by commas, not all commas separate top-level selectors.
1541 - * This method only separates top-level selectors, so some commas may appear inside
1542 - * strings, nested selectors, and comments. Leading and trailing CSS whitespace is
1543 - * trimmed from the returned list items.
1544 - *
1545 - * Non-selector content, such as comments, are retained in the list in the same item
1546 - * as the selector content they follow.
1547 - *
1548 - * Example:
1549 - *
1550 - * array( '.wp-block' ) === self::split_selector_list( '.wp-block' );
1551 - * array( '.one', '.two' ) === self::split_selector_list( '.one, .two' );
1552 - *
1553 - * // Nested selector lists are retained within their containing selector.
1554 - * array( ':is(.a, .b)', 'c' ) === self::split_selector_list( ':is(.a, .b), .c' );
1555 - *
1556 - * // Commas within strings do not separate selectors.
1557 - * $selectors = self::split_selector_list( '[data-label="Save, continue"],.fallback' );
1558 - * $selectors === array( '[data-label="Save, continue"]', '.fallback' )
1559 - *
1560 - * array( 'lang(zh, "*-hant")', '.foo' ) === self::split_selector_list( 'lang(zh, "*-hant"), .foo' );
1561 - *
1562 - * // Identifiers may contain escaped commas.
1563 - * array( '.foo\,bar', '.baz' ) === self::split_selector_list( '.foo\,bar,.baz' );
1564 - *
1565 - * // Comments stay with the selector they follow.
1566 - * array( '.a /* a, the first *\/', '.b' ) === self::split_selector_list( '.a /* a, the first *\/,.b' );
1567 - *
1568 - * @link https://www.w3.org/TR/selectors/#parse-selector
1569 - * @link https://www.w3.org/TR/css-syntax-3/
1570 - *
1571 - * @param string $selector CSS selector list.
1572 - * @return string[] Selectors.
1573 - */
1574 - protected static function split_selector_list( $selector ): array {
1575 - if ( ! str_contains( $selector, ',' ) ) {
1576 - // See note on trimming CSS whitespace in main loop.
1577 - return array( trim( $selector, " \t\n" ) );
1578 - }
1579 -
1580 - $selectors = array();
1581 - $selector_length = strlen( $selector );
1582 - $parentheses_depth = 0;
1583 - $at = 0;
1584 - $was_at = 0;
1585 -
1586 - while ( $at < $selector_length ) {
1587 - $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1588 - if ( $next_at >= $selector_length ) {
1589 - break;
1590 - }
1591 -
1592 - $next_cp = $selector[ $next_at ];
1593 -
1594 - // Escaped syntax characters do not act as delimiters.
1595 - if ( '\\' === $next_cp ) {
1596 - $at = min( $next_at + 2, $selector_length );
1597 - continue;
1598 - }
1599 -
1600 - /*
1601 - * Start of a parenthesized expression, which maintains a stack of parentheses.
1602 - * For the sake of this function, no selector list will be split inside parentheses.
1603 - * Therefore it’s possible to jump ahead until this list completes.
1604 - */
1605 - if ( '(' === $next_cp || ')' === $next_cp ) {
1606 - $parentheses_depth += '(' === $next_cp ? 1 : -1;
1607 - $at = $next_at + 1;
1608 - continue;
1609 - }
1610 -
1611 - // Start of a string, which will be incorporated into the selector in which it’s found.
1612 - if ( "'" === $next_cp || '"' === $next_cp ) {
1613 - $end_of_string = $next_at + 1;
1614 - while ( $end_of_string < $selector_length ) {
1615 - $end_of_string += strcspn( $selector, "{$next_cp}\\", $end_of_string );
1616 - if ( $end_of_string >= $selector_length ) {
1617 - break;
1618 - }
1619 -
1620 - $end_cp = $selector[ $end_of_string ];
1621 -
1622 - // Skip escaped characters.
1623 - if ( '\\' === $end_cp ) {
1624 - $end_of_string = $end_of_string + 2;
1625 - continue;
1626 - }
1627 -
1628 - if ( $next_cp === $end_cp ) {
1629 - ++$end_of_string;
1630 - break;
1631 - }
1632 -
1633 - ++$end_of_string;
1634 - }
1635 -
1636 - $at = $end_of_string;
1637 - continue;
1638 - }
1639 -
1640 - // Start of a comment, which will be incorporated into the selector in which it’s found.
1641 - if ( '/' === $next_cp && ( $next_at + 1 ) < $selector_length && '*' === $selector[ $next_at + 1 ] ) {
1642 - $comment_end_at = strpos( $selector, '*/', $next_at + 1 );
1643 - $is_terminated = false !== $comment_end_at;
1644 - $after_comment = $is_terminated ? $comment_end_at + 2 : strlen( $selector );
1645 - $at = $after_comment;
1646 - continue;
1647 - }
1648 -
1649 - // Start of a CDO or CDC, which will be incorporated into the selector in which it’s found.
1650 - if (
1651 - ( '<' === $next_cp && 0 === substr_compare( $selector, '<!--', $next_at, 4 ) ) ||
1652 - ( '-' === $next_cp && 0 === substr_compare( $selector, '-->', $next_at, 3 ) )
1653 - ) {
1654 - $at = $next_at + ( '<' === $next_cp ? 4 : 3 );
1655 - continue;
1656 - }
1657 -
1658 - // Everything else is either a comma token or part of a selector.
1659 - if ( ',' === $next_cp && 0 === $parentheses_depth ) {
1660 - /**
1661 - * Trim each selector so that downstream code doesn’t see whitespace
1662 - * as the first character in a selector and get confused.
1663 - *
1664 - * There is inconsistency in this because comments and other syntax
1665 - * are included which are also not part of the selector itself, but
1666 - * a tradeoff is made between removing common syntax which carries
1667 - * no meaning and rarer syntax which leaves auxiliary information.
1668 - *
1669 - * > A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
1670 - * > Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are
1671 - * > not included in this definition, as they are converted
1672 - * > to U+000A LINE FEED during preprocessing.
1673 - *
1674 - * @link https://www.w3.org/TR/css-syntax/#whitespace
1675 - * @link https://www.w3.org/TR/css-syntax/#newline
1676 - */
1677 - $selectors[] = trim( substr( $selector, $was_at, $next_at - $was_at ), " \t\n" );
1678 - $at = $next_at + 1;
1679 - $was_at = $at;
1680 - continue;
1681 - }
1682 -
1683 - $at = $next_at + 1;
1684 - }
1685 -
1686 - if ( $was_at < $selector_length ) {
1687 - // See note on trimming CSS whitespace in main loop.
1688 - $selectors[] = trim( substr( $selector, $was_at ), " \t\n" );
1689 - }
1690 -
1691 - return $selectors;
1692 - }
1693 -
1694 - /**
1695 875 * Returns the metadata for each block.
1696 876 *
1697 877 * Example:
1698 878 *
@@ -1717,39 +897,19 @@
1717 897 *
1718 898 * @since 5.8.0
1719 899 * @since 5.9.0 Added `duotone` key with CSS selector.
1720 900 * @since 6.1.0 Added `features` key with block support feature level selectors.
1721 - * @since 6.6.0 Added non-core block style variations to generated metadata.
1722 901 *
1723 902 * @return array Block metadata.
1724 903 */
1725 904 protected static function get_blocks_metadata() {
1726 905 // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes.
1727 - $registry = WP_Block_Type_Registry::get_instance();
1728 - $blocks = $registry->get_all_registered();
1729 - $style_registry = WP_Block_Styles_Registry::get_instance();
906 + $registry = WP_Block_Type_Registry::get_instance();
907 + $blocks = $registry->get_all_registered();
1730 908
1731 909 // Is there metadata for all currently registered blocks?
1732 910 $blocks = array_diff_key( $blocks, static::$blocks_metadata );
1733 911 if ( empty( $blocks ) ) {
1734 - /*
1735 - * New block styles may have been registered within WP_Block_Styles_Registry.
1736 - * Update block metadata for any new block style variations.
1737 - */
1738 - $registered_styles = $style_registry->get_all_registered();
1739 - foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
1740 - if ( ! empty( $registered_styles[ $block_name ] ) ) {
1741 - $style_selectors = $block_metadata['styleVariations'] ?? array();
1742 -
1743 - foreach ( $registered_styles[ $block_name ] as $block_style ) {
1744 - if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
1745 - $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
1746 - }
1747 - }
1748 -
1749 - static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1750 - }
1751 - }
1752 912 return static::$blocks_metadata;
1753 913 }
1754 914
1755 915 foreach ( $blocks as $block_name => $block_type ) {
@@ -1771,9 +931,9 @@
1771 931 $duotone_support = $block_type->supports['color']['__experimentalDuotone'] ?? null;
1772 932
1773 933 if ( $duotone_support ) {
1774 934 $root_selector = wp_get_block_css_selector( $block_type );
1775 - $duotone_selector = static::scope_selector( $root_selector, $duotone_support );
935 + $duotone_selector = WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $duotone_support );
1776 936 }
1777 937 }
1778 938
1779 939 if ( null !== $duotone_selector ) {
@@ -1780,29 +940,16 @@
1780 940 static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
1781 941 }
1782 942
1783 943 // If the block has style variations, append their selectors to the block metadata.
1784 - $style_selectors = array();
1785 944 if ( ! empty( $block_type->styles ) ) {
945 + $style_selectors = array();
1786 946 foreach ( $block_type->styles as $style ) {
1787 - $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
947 + // The style variation classname is duplicated in the selector to ensure that it overrides core block styles.
948 + $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1788 949 }
1789 - }
1790 -
1791 - // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
1792 - $registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
1793 - foreach ( $registered_styles as $style ) {
1794 - $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1795 - }
1796 -
1797 - if ( ! empty( $style_selectors ) ) {
1798 950 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1799 951 }
1800 -
1801 - // If the block has custom states defined in block.json, store their selectors.
1802 - if ( ! empty( $block_type->selectors['states'] ) && is_array( $block_type->selectors['states'] ) ) {
1803 - static::$blocks_metadata[ $block_name ]['states'] = $block_type->selectors['states'];
1804 - }
1805 952 }
1806 953
1807 954 return static::$blocks_metadata;
1808 955 }
@@ -1812,9 +959,8 @@
1812 959 *
1813 960 * It is recursive and modifies the input in-place.
1814 961 *
1815 962 * @since 5.8.0
1816 - * @since 7.0.0 Added type validation for boolean values.
1817 963 *
1818 964 * @param array $tree Input to process.
1819 965 * @param array $schema Schema to adhere to.
1820 966 * @return array The modified $tree.
@@ -1819,51 +965,23 @@
1819 965 * @param array $schema Schema to adhere to.
1820 966 * @return array The modified $tree.
1821 967 */
1822 968 protected static function remove_keys_not_in_schema( $tree, $schema ) {
1823 - if ( ! is_array( $tree ) ) {
1824 - return $tree;
1825 - }
969 + $tree = array_intersect_key( $tree, $schema );
1826 970
1827 - foreach ( $tree as $key => $value ) {
1828 - // Remove keys not in the schema or with null/empty values.
1829 - if ( ! array_key_exists( $key, $schema ) ) {
1830 - unset( $tree[ $key ] );
971 + foreach ( $schema as $key => $data ) {
972 + if ( ! isset( $tree[ $key ] ) ) {
1831 973 continue;
1832 974 }
1833 975
1834 - // Validate type if schema specifies a boolean marker.
1835 - if ( is_bool( $schema[ $key ] ) ) {
1836 - // Schema expects a boolean value - validate the input matches.
1837 - if ( ! is_bool( $value ) ) {
1838 - unset( $tree[ $key ] );
1839 - continue;
1840 - }
1841 - // Type matches, keep the value and continue to next key.
1842 - continue;
1843 - }
976 + if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
977 + $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
1844 978
1845 - if ( is_array( $schema[ $key ] ) ) {
1846 - if ( ! is_array( $value ) ) {
979 + if ( empty( $tree[ $key ] ) ) {
1847 980 unset( $tree[ $key ] );
1848 - } elseif ( wp_is_numeric_array( $value ) ) {
1849 - // If indexed, process each item in the array.
1850 - foreach ( $value as $item_key => $item_value ) {
1851 - if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
1852 - $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
1853 - } else {
1854 - // If the schema does not define a further structure, keep the value as is.
1855 - $tree[ $key ][ $item_key ] = $item_value;
1856 - }
1857 - }
1858 - } else {
1859 - // If associative, process as a single object.
1860 - $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );
1861 -
1862 - if ( empty( $tree[ $key ] ) ) {
1863 - unset( $tree[ $key ] );
1864 - }
1865 981 }
982 + } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
983 + unset( $tree[ $key ] );
1866 984 }
1867 985 }
1868 986
1869 987 return $tree;
@@ -1904,23 +1022,17 @@
1904 1022 * the theme.json structure this object represents.
1905 1023 *
1906 1024 * @since 5.8.0
1907 1025 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
1908 - * @since 6.6.0 Added option to skip root layout or block style variation styles.
1909 1026 *
1910 1027 * @param array $types Types of styles to load. Will load all by default. It accepts:
1911 1028 * - `variables`: only the CSS Custom Properties for presets & custom ones.
1912 1029 * - `styles`: only the styles section in theme.json.
1913 1030 * - `presets`: only the classes for the presets.
1914 - * - `custom-css`: only the custom CSS.
1915 1031 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
1916 1032 * @param array $options An array of options for now used for internal purposes only (may change without notice).
1917 - * The options currently supported are:
1918 - * - 'scope' that makes sure all style are scoped to a given selector
1919 - * - `root_selector` which overwrites and forces a given selector to be used on the root node
1920 - * - `skip_root_layout_styles` which omits root layout styles from the generated stylesheet.
1921 - * - `base_layout_styles` which when true generates only base layout styles without alignment rules. Defaults to false.
1922 - * - `include_block_style_variations` which includes CSS for block style variations.
1033 + * The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
1034 + * and root_selector which overwrites and forces a given selector to be used on the root node.
1923 1035 * @return string The resulting stylesheet.
1924 1036 */
1925 1037 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
1926 1038 if ( null === $origins ) {
@@ -1939,9 +1051,9 @@
1939 1051 }
1940 1052 }
1941 1053
1942 1054 $blocks_metadata = static::get_blocks_metadata();
1943 - $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
1055 + $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
1944 1056 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
1945 1057
1946 1058 $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
1947 1059 $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
@@ -1950,9 +1062,9 @@
1950 1062 foreach ( $setting_nodes as &$node ) {
1951 1063 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1952 1064 }
1953 1065 foreach ( $style_nodes as &$node ) {
1954 - $node = static::scope_style_node_selectors( $options['scope'], $node );
1066 + $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1955 1067 }
1956 1068 unset( $node );
1957 1069 }
1958 1070
@@ -1960,9 +1072,9 @@
1960 1072 if ( false !== $root_settings_key ) {
1961 1073 $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
1962 1074 }
1963 1075 if ( false !== $root_style_key ) {
1964 - $style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
1076 + $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
1965 1077 }
1966 1078 }
1967 1079
1968 1080 $stylesheet = '';
@@ -1971,12 +1083,46 @@
1971 1083 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
1972 1084 }
1973 1085
1974 1086 if ( in_array( 'styles', $types, true ) ) {
1975 - if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
1976 - $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ], $options );
1087 + if ( false !== $root_style_key ) {
1088 + $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
1977 1089 }
1978 1090 $stylesheet .= $this->get_block_classes( $style_nodes );
1091 + } elseif ( in_array( 'base-layout-styles', $types, true ) ) {
1092 + $root_selector = static::ROOT_BLOCK_SELECTOR;
1093 + $columns_selector = '.wp-block-columns';
1094 + $post_template_selector = '.wp-block-post-template';
1095 + if ( ! empty( $options['scope'] ) ) {
1096 + $root_selector = static::scope_selector( $options['scope'], $root_selector );
1097 + $columns_selector = static::scope_selector( $options['scope'], $columns_selector );
1098 + $post_template_selector = static::scope_selector( $options['scope'], $post_template_selector );
1099 + }
1100 + if ( ! empty( $options['root_selector'] ) ) {
1101 + $root_selector = $options['root_selector'];
1102 + }
1103 + // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
1104 + // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
1105 + $base_styles_nodes = array(
1106 + array(
1107 + 'path' => array( 'styles' ),
1108 + 'selector' => $root_selector,
1109 + ),
1110 + array(
1111 + 'path' => array( 'styles', 'blocks', 'core/columns' ),
1112 + 'selector' => $columns_selector,
1113 + 'name' => 'core/columns',
1114 + ),
1115 + array(
1116 + 'path' => array( 'styles', 'blocks', 'core/post-template' ),
1117 + 'selector' => $post_template_selector,
1118 + 'name' => 'core/post-template',
1119 + ),
1120 + );
1121 +
1122 + foreach ( $base_styles_nodes as $base_style_node ) {
1123 + $stylesheet .= $this->get_layout_styles( $base_style_node );
1124 + }
1979 1125 }
1980 1126
1981 1127 if ( in_array( 'presets', $types, true ) ) {
1982 1128 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
@@ -1981,14 +1127,8 @@
1981 1127 if ( in_array( 'presets', $types, true ) ) {
1982 1128 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
1983 1129 }
1984 1130
1985 - // Load the custom CSS last so it has the highest specificity.
1986 - if ( in_array( 'custom-css', $types, true ) ) {
1987 - // Add the global styles root CSS.
1988 - $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
1989 - }
1990 -
1991 1131 return $stylesheet;
1992 1132 }
1993 1133
1994 1134 /**
@@ -1999,25 +1139,18 @@
1999 1139 * @param string $css The CSS to process.
2000 1140 * @param string $selector The selector to nest.
2001 1141 * @return string The processed CSS.
2002 1142 */
2003 - public static function process_blocks_custom_css( $css, $selector ) {
1143 + protected function process_blocks_custom_css( $css, $selector ) {
2004 1144 $processed_css = '';
2005 1145
2006 - if ( empty( $css ) ) {
2007 - return $processed_css;
2008 - }
2009 -
2010 1146 // Split CSS nested rules.
2011 1147 $parts = explode( '&', $css );
2012 1148 foreach ( $parts as $part ) {
2013 - if ( empty( $part ) ) {
2014 - continue;
2015 - }
2016 1149 $is_root_css = ( ! str_contains( $part, '{' ) );
2017 1150 if ( $is_root_css ) {
2018 1151 // If the part doesn't contain braces, it applies to the root level.
2019 - $processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
1152 + $processed_css .= trim( $selector ) . '{' . trim( $part ) . '}';
2020 1153 } else {
2021 1154 // If the part contains braces, it's a nested CSS rule.
2022 1155 $part = explode( '{', str_replace( '}', '', $part ) );
2023 1156 if ( count( $part ) !== 2 ) {
@@ -2024,26 +1157,12 @@
2024 1157 continue;
2025 1158 }
2026 1159 $nested_selector = $part[0];
2027 1160 $css_value = $part[1];
2028 -
2029 - /*
2030 - * Handle pseudo elements such as ::before, ::after etc. Regex will also
2031 - * capture any leading combinator such as >, +, or ~, as well as spaces.
2032 - * This allows pseudo elements as descendants e.g. `.parent ::before`.
2033 - */
2034 - $matches = array();
2035 - $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
2036 - $pseudo_part = $has_pseudo_element ? $matches[1] : '';
2037 - $nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;
2038 -
2039 - // Finalize selector and re-append pseudo element if required.
2040 - $part_selector = str_starts_with( $nested_selector, ' ' )
1161 + $part_selector = str_starts_with( $nested_selector, ' ' )
2041 1162 ? static::scope_selector( $selector, $nested_selector )
2042 1163 : static::append_to_selector( $selector, $nested_selector );
2043 - $final_selector = ":root :where($part_selector)$pseudo_part";
2044 -
2045 - $processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
1164 + $processed_css .= $part_selector . '{' . trim( $css_value ) . '}';
2046 1165 }
2047 1166 }
2048 1167 return $processed_css;
2049 1168 }
@@ -2051,77 +1170,30 @@
2051 1170 /**
2052 1171 * Returns the global styles custom css.
2053 1172 *
2054 1173 * @since 6.2.0
2055 - * @deprecated 6.7.0 Use {@see 'get_stylesheet'} instead.
2056 1174 *
2057 1175 * @return string The global styles custom CSS.
2058 1176 */
2059 1177 public function get_custom_css() {
2060 - _deprecated_function( __METHOD__, '6.7.0', 'get_stylesheet' );
2061 - $block_custom_css = '';
2062 - $block_nodes = $this->get_block_custom_css_nodes();
2063 - foreach ( $block_nodes as $node ) {
2064 - // The node selector will have its specificity set to 0-1-0 within process_blocks_custom_css.
2065 - $block_custom_css .= $this->get_block_custom_css( $node['css'], $node['selector'] );
2066 - }
1178 + // Add the global styles root CSS.
1179 + $stylesheet = $this->theme_json['styles']['css'] ?? '';
2067 1180
2068 - return $this->get_base_custom_css() . $block_custom_css;
2069 - }
2070 -
2071 - /**
2072 - * Returns the global styles base custom CSS.
2073 - * This function is deprecated; please do not sync to core.
2074 - *
2075 - * @return string The global styles base custom CSS.
2076 - */
2077 - public function get_base_custom_css() {
2078 - _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_stylesheet' );
2079 - return $this->theme_json['styles']['css'] ?? '';
2080 - }
2081 -
2082 - /**
2083 - * Returns the block nodes with custom CSS.
2084 - * This function is deprecated; please do not sync to core.
2085 - *
2086 - * @return array The block nodes.
2087 - */
2088 - public function get_block_custom_css_nodes() {
2089 - _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_block_nodes' );
2090 - $block_nodes = array();
2091 -
2092 1181 // Add the global styles block CSS.
2093 1182 if ( isset( $this->theme_json['styles']['blocks'] ) ) {
2094 1183 foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
2095 1184 $custom_block_css = $this->theme_json['styles']['blocks'][ $name ]['css'] ?? null;
2096 1185 if ( $custom_block_css ) {
2097 - $block_nodes[] = array(
2098 - 'name' => $name,
2099 - 'selector' => static::$blocks_metadata[ $name ]['selector'],
2100 - 'css' => $custom_block_css,
2101 - );
1186 + $selector = static::$blocks_metadata[ $name ]['selector'];
1187 + $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
2102 1188 }
2103 1189 }
2104 1190 }
2105 1191
2106 - return $block_nodes;
1192 + return $stylesheet;
2107 1193 }
2108 1194
2109 1195 /**
2110 - * Returns the global styles custom CSS for a single block.
2111 - * This function is deprecated; please do not sync to core.
2112 - *
2113 - * @param array $css The block css node.
2114 - * @param string $selector The block selector.
2115 - *
2116 - * @return string The global styles custom CSS for the block.
2117 - */
2118 - public function get_block_custom_css( $css, $selector ) {
2119 - _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_styles_for_block' );
2120 - return $this->process_blocks_custom_css( $css, $selector );
2121 - }
2122 -
2123 - /**
2124 1196 * Returns the page templates of the active theme.
2125 1197 *
2126 1198 * @since 5.9.0
2127 1199 *
@@ -2135,10 +1207,10 @@
2135 1207
2136 1208 foreach ( $this->theme_json['customTemplates'] as $item ) {
2137 1209 if ( isset( $item['name'] ) ) {
2138 1210 $custom_templates[ $item['name'] ] = array(
2139 - 'title' => $item['title'] ?? '',
2140 - 'postTypes' => $item['postTypes'] ?? array( 'page' ),
1211 + 'title' => isset( $item['title'] ) ? $item['title'] : '',
1212 + 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
2141 1213 );
2142 1214 }
2143 1215 }
2144 1216 return $custom_templates;
@@ -2159,10 +1231,10 @@
2159 1231
2160 1232 foreach ( $this->theme_json['templateParts'] as $item ) {
2161 1233 if ( isset( $item['name'] ) ) {
2162 1234 $template_parts[ $item['name'] ] = array(
2163 - 'title' => $item['title'] ?? '',
2164 - 'area' => $item['area'] ?? '',
1235 + 'title' => isset( $item['title'] ) ? $item['title'] : '',
1236 + 'area' => isset( $item['area'] ) ? $item['area'] : '',
2165 1237 );
2166 1238 }
2167 1239 }
2168 1240 return $template_parts;
@@ -2207,12 +1279,11 @@
2207 1279 *
2208 1280 * @since 6.1.0
2209 1281 *
2210 1282 * @param array $block_metadata Metadata about the block to get styles for.
2211 - * @param array $options Optional. An array of options for now used for internal purposes only.
2212 1283 * @return string Layout styles for the block.
2213 1284 */
2214 - protected function get_layout_styles( $block_metadata, $options = array() ) {
1285 + protected function get_layout_styles( $block_metadata ) {
2215 1286 $block_rules = '';
2216 1287 $block_type = null;
2217 1288
2218 1289 // Skip outputting layout styles if explicitly disabled.
@@ -2226,14 +1297,14 @@
2226 1297 return $block_rules;
2227 1298 }
2228 1299 }
2229 1300
2230 - $selector = $block_metadata['selector'] ?? '';
1301 + $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
2231 1302 $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] );
2232 1303 $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.
2233 - $node = $options['node'] ?? _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
1304 + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
2234 1305 $layout_definitions = gutenberg_get_layout_definitions();
2235 - $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
1306 + $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
2236 1307
2237 1308 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
2238 1309 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
2239 1310 if ( $has_block_gap_support || $has_fallback_gap_support ) {
@@ -2302,9 +1373,9 @@
2302 1373 $class_name,
2303 1374 $spacing_rule['selector']
2304 1375 );
2305 1376 } else {
2306 - $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
1377 + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(%s .%s) %s' : '%s-%s%s';
2307 1378 $layout_selector = sprintf(
2308 1379 $format,
2309 1380 $selector,
2310 1381 $class_name,
@@ -2356,14 +1427,8 @@
2356 1427
2357 1428 foreach ( $base_style_rules as $base_style_rule ) {
2358 1429 $declarations = array();
2359 1430
2360 - // Skip outputting base styles for flow and constrained layout types when base_layout_styles is enabled.
2361 - // These themes don't use .wp-site-blocks wrapper, so these layout-specific alignment styles aren't needed.
2362 - if ( ! empty( $options['base_layout_styles'] ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) {
2363 - continue;
2364 - }
2365 -
2366 1431 if (
2367 1432 isset( $base_style_rule['selector'] ) &&
2368 1433 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
2369 1434 ! empty( $base_style_rule['rules'] )
@@ -2368,18 +1433,8 @@
2368 1433 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
2369 1434 ! empty( $base_style_rule['rules'] )
2370 1435 ) {
2371 1436 foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
2372 - // Skip rules that reference content size or wide size if they are not defined in the theme.json.
2373 - if (
2374 - is_string( $css_value ) &&
2375 - ( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) &&
2376 - ! isset( $this->theme_json['settings']['layout']['contentSize'] ) &&
2377 - ! isset( $this->theme_json['settings']['layout']['wideSize'] )
2378 - ) {
2379 - continue;
2380 - }
2381 -
2382 1437 if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
2383 1438 $declarations[] = array(
2384 1439 'name' => $css_property,
2385 1440 'value' => $css_value,
@@ -2387,9 +1442,10 @@
2387 1442 }
2388 1443 }
2389 1444
2390 1445 $layout_selector = sprintf(
2391 - '.%s%s',
1446 + '%s .%s%s',
1447 + $selector,
2392 1448 $class_name,
2393 1449 $base_style_rule['selector']
2394 1450 );
2395 1451 $block_rules .= static::to_ruleset( $layout_selector, $declarations );
@@ -2397,13 +1453,8 @@
2397 1453 }
2398 1454 }
2399 1455 }
2400 1456 }
2401 -
2402 - if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) {
2403 - $block_rules = $options['media_query'] . '{' . $block_rules . '}';
2404 - }
2405 -
2406 1457 return $block_rules;
2407 1458 }
2408 1459
2409 1460 /**
@@ -2424,9 +1475,9 @@
2424 1475 * .has-value-gradient-background {
2425 1476 * background: value;
2426 1477 * }
2427 1478 *
2428 - * :where(p).has-value-gradient-background {
1479 + * p.has-value-gradient-background {
2429 1480 * background: value;
2430 1481 * }
2431 1482 *
2432 1483 * @since 5.9.0
@@ -2478,53 +1529,18 @@
2478 1529 if ( null === $metadata['selector'] ) {
2479 1530 continue;
2480 1531 }
2481 1532
2482 - $selector = $metadata['selector'];
2483 - $feature_selectors = $metadata['selectors'] ?? array();
2484 - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1533 + $selector = $metadata['selector'];
2485 1534
2486 - /*
2487 - * Group preset declarations by selector. Blocks that define
2488 - * feature-level selectors need their preset CSS variables
2489 - * output under that feature selector instead of the block's
2490 - * root selector.
2491 - */
2492 - $vars_by_selector = array();
2493 - $vars_by_selector[ $selector ] = array();
2494 -
2495 - foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2496 - if ( empty( $preset_metadata['css_vars'] ) ) {
2497 - continue;
2498 - }
2499 -
2500 - $values_by_slug = static::get_settings_values_by_slug( $node, $preset_metadata, $origins );
2501 - if ( empty( $values_by_slug ) ) {
2502 - continue;
2503 - }
2504 -
2505 - $target = static::get_feature_selector( $feature_selectors, $preset_metadata['path'][0], $selector );
2506 -
2507 - if ( ! isset( $vars_by_selector[ $target ] ) ) {
2508 - $vars_by_selector[ $target ] = array();
2509 - }
2510 -
2511 - foreach ( $values_by_slug as $slug => $value ) {
2512 - $vars_by_selector[ $target ][] = array(
2513 - 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
2514 - 'value' => $value,
2515 - );
2516 - }
1535 + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1536 + $declarations = static::compute_preset_vars( $node, $origins );
1537 + $theme_vars_declarations = static::compute_theme_vars( $node );
1538 + foreach ( $theme_vars_declarations as $theme_vars_declaration ) {
1539 + $declarations[] = $theme_vars_declaration;
2517 1540 }
2518 1541
2519 - // Theme vars always use the block's default selector.
2520 - foreach ( static::compute_theme_vars( $node ) as $theme_var ) {
2521 - $vars_by_selector[ $selector ][] = $theme_var;
2522 - }
2523 -
2524 - foreach ( $vars_by_selector as $rule_selector => $declarations ) {
2525 - $stylesheet .= static::to_ruleset( $rule_selector, $declarations );
2526 - }
1542 + $stylesheet .= static::to_ruleset( $selector, $declarations );
2527 1543 }
2528 1544
2529 1545 return $stylesheet;
2530 1546 }
@@ -2529,44 +1545,12 @@
2529 1545 return $stylesheet;
2530 1546 }
2531 1547
2532 1548 /**
2533 - * Returns the appropriate selector for a block support feature's
2534 - * preset CSS variables.
2535 - *
2536 - * If the block defines a feature-level selector (as a string or an
2537 - * object with a `root` key), that selector is returned. Otherwise,
2538 - * the block's default selector is used.
2539 - *
2540 - * @param array<string, string|array<string, string>> $feature_selectors The block's feature selectors map.
2541 - * @param string $feature_key The feature to look up (e.g. 'dimensions').
2542 - * @param string $default_selector Fallback selector.
2543 - * @return string The resolved selector.
2544 - */
2545 - private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string {
2546 - if ( ! isset( $feature_selectors[ $feature_key ] ) ) {
2547 - return $default_selector;
2548 - }
2549 -
2550 - $feature = $feature_selectors[ $feature_key ];
2551 -
2552 - if ( is_string( $feature ) ) {
2553 - return $feature;
2554 - }
2555 -
2556 - if ( isset( $feature['root'] ) && is_string( $feature['root'] ) ) {
2557 - return $feature['root'];
2558 - }
2559 -
2560 - return $default_selector;
2561 - }
2562 -
2563 - /**
2564 1549 * Given a selector and a declaration list,
2565 1550 * creates the corresponding ruleset.
2566 1551 *
2567 1552 * @since 5.8.0
2568 - * @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.).
2569 1553 *
2570 1554 * @param string $selector CSS selector.
2571 1555 * @param array $declarations List of declarations.
2572 1556 * @return string The resulting CSS ruleset.
@@ -2578,20 +1562,9 @@
2578 1562
2579 1563 $declaration_block = array_reduce(
2580 1564 $declarations,
2581 1565 static function ( $carry, $element ) {
2582 - $value = $element['value'];
2583 -
2584 - if ( is_numeric( $value ) ) {
2585 - $value = (string) $value;
2586 - }
2587 -
2588 - if ( ! is_string( $value ) ) {
2589 - return $carry;
2590 - }
2591 -
2592 - return $carry .= $element['name'] . ': ' . $value . ';';
2593 - },
1566 + return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
2594 1567 ''
2595 1568 );
2596 1569
2597 1570 return $selector . '{' . $declaration_block . '}';
@@ -2609,9 +1582,9 @@
2609 1582 * @param array $origins List of origins to process.
2610 1583 * @return string The result of processing the presets.
2611 1584 */
2612 1585 protected static function compute_preset_classes( $settings, $selector, $origins ) {
2613 - if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
1586 + if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
2614 1587 // Classes at the global level do not need any CSS prefixed,
2615 1588 // and we don't want to increase its specificity.
2616 1589 $selector = '';
2617 1590 }
@@ -2627,18 +1600,10 @@
2627 1600 foreach ( $slugs as $slug ) {
2628 1601 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2629 1602 $class_name = static::replace_slug_in_string( $class, $slug );
2630 1603
2631 - /*
2632 - * $selector is often empty (root-level presets), in which case the
2633 - * bare class is used. For block-level presets the block selector is
2634 - * wrapped in `:where()` so the class keeps the same 0-1-0 specificity
2635 - * as a root-level preset. Without this, block-level palette rules
2636 - * (e.g. `p.has-x-color`) out-rank equally-important rules that also
2637 - * target the same property at 0-1-0, such as per-instance responsive
2638 - * state styles.
2639 - */
2640 - $new_selector = '' === $selector ? $class_name : ':where(' . $selector . ')' . $class_name;
1604 + // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
1605 + $new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
2641 1606 $stylesheet .= static::to_ruleset(
2642 1607 $new_selector,
2643 1608 array(
2644 1609 array(
@@ -2671,18 +1636,16 @@
2671 1636 * @param string $selector Original selector.
2672 1637 * @return string Scoped selector.
2673 1638 */
2674 1639 public static function scope_selector( $scope, $selector ) {
2675 - if ( ! $scope || ! $selector ) {
2676 - return $selector;
2677 - }
1640 + $scopes = explode( ',', $scope );
1641 + $selectors = explode( ',', $selector );
2678 1642
2679 - $scopes = static::split_selector_list( $scope );
2680 - $selectors = static::split_selector_list( $selector );
2681 -
2682 1643 $selectors_scoped = array();
2683 1644 foreach ( $scopes as $outer ) {
2684 1645 foreach ( $selectors as $inner ) {
1646 + $outer = trim( $outer );
1647 + $inner = trim( $inner );
2685 1648 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2686 1649 $selectors_scoped[] = $outer . ' ' . $inner;
2687 1650 } elseif ( empty( $outer ) ) {
2688 1651 $selectors_scoped[] = $inner;
@@ -2696,41 +1659,8 @@
2696 1659 return $result;
2697 1660 }
2698 1661
2699 1662 /**
2700 - * Scopes the selectors for a given style node. This includes the primary
2701 - * selector, i.e. `$node['selector']`, as well as any custom selectors for
2702 - * features and subfeatures, e.g. `$node['selectors']['border']` etc.
2703 - *
2704 - * @since 6.6.0
2705 - *
2706 - * @param string $scope Selector to scope to.
2707 - * @param array $node Style node with selectors to scope.
2708 - *
2709 - * @return array Node with updated selectors.
2710 - */
2711 - protected static function scope_style_node_selectors( $scope, $node ) {
2712 - $node['selector'] = static::scope_selector( $scope, $node['selector'] );
2713 -
2714 - if ( empty( $node['selectors'] ) ) {
2715 - return $node;
2716 - }
2717 -
2718 - foreach ( $node['selectors'] as $feature => $selector ) {
2719 - if ( is_string( $selector ) ) {
2720 - $node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
2721 - }
2722 - if ( is_array( $selector ) ) {
2723 - foreach ( $selector as $subfeature => $subfeature_selector ) {
2724 - $node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
2725 - }
2726 - }
2727 - }
2728 -
2729 - return $node;
2730 - }
2731 -
2732 - /**
2733 1663 * Gets preset values keyed by slugs based on settings and metadata.
2734 1664 *
2735 1665 * <code>
2736 1666 * $settings = array(
@@ -2758,9 +1688,8 @@
2758 1688 * // );
2759 1689 * </code>
2760 1690 *
2761 1691 * @since 5.9.0
2762 - * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
2763 1692 *
2764 1693 * @param array $settings Settings to process.
2765 1694 * @param array $preset_metadata One of the PRESETS_METADATA values.
2766 1695 * @param array $origins List of origins to process.
@@ -2785,9 +1714,9 @@
2785 1714 isset( $preset_metadata['value_func'] ) &&
2786 1715 is_callable( $preset_metadata['value_func'] )
2787 1716 ) {
2788 1717 $value_func = $preset_metadata['value_func'];
2789 - $value = call_user_func( $value_func, $preset, $settings );
1718 + $value = call_user_func( $value_func, $preset );
2790 1719 } else {
2791 1720 // If we don't have a value, then don't add it to the result.
2792 1721 continue;
2793 1722 }
@@ -2977,9 +1906,9 @@
2977 1906 *
2978 1907 * ```php
2979 1908 * array(
2980 1909 * 'name' => 'property_name',
2981 - * 'value' => 'property_value',
1910 + * 'value' => 'property_value,
2982 1911 * )
2983 1912 * ```
2984 1913 *
2985 1914 * @since 5.8.0
@@ -2984,11 +1913,8 @@
2984 1913 *
2985 1914 * @since 5.8.0
2986 1915 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
2987 1916 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
2988 - * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2989 - * @since 6.6.0 Passing current theme JSON settings to wp_get_typography_font_size_value(). Using style engine to correctly fetch background CSS values.
2990 - * @since 6.7.0 Allow ref resolution of background properties.
2991 1917 *
2992 1918 * @param array $styles Styles to process.
2993 1919 * @param array $settings Theme settings.
2994 1920 * @param array $properties Properties metadata.
@@ -2997,31 +1923,25 @@
2997 1923 * @param boolean $use_root_padding Whether to add custom properties at root level.
2998 1924 * @return array Returns the modified $declarations.
2999 1925 */
3000 1926 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
1927 + if ( null === $properties ) {
1928 + $properties = static::PROPERTIES_METADATA;
1929 + }
1930 +
1931 + $declarations = array();
3001 1932 if ( empty( $styles ) ) {
3002 - return array();
1933 + return $declarations;
3003 1934 }
3004 1935
3005 - if ( null === $properties ) {
3006 - $properties = static::PROPERTIES_METADATA;
3007 - }
3008 - $declarations = array();
3009 1936 $root_variable_duplicates = array();
3010 - $root_style_length = strlen( '--wp--style--root--' );
3011 1937
3012 1938 foreach ( $properties as $css_property => $value_path ) {
3013 - if ( ! is_array( $value_path ) ) {
3014 - continue;
3015 - }
1939 + $value = static::get_property_value( $styles, $value_path, $theme_json );
3016 1940
3017 - $is_root_style = str_starts_with( $css_property, '--wp--style--root--' );
3018 - if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
1941 + if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
3019 1942 continue;
3020 1943 }
3021 -
3022 - $value = static::get_property_value( $styles, $value_path, $theme_json );
3023 -
3024 1944 // Root-level padding styles don't currently support strings with CSS shorthand values.
3025 1945 // This may change: https://github.com/WordPress/gutenberg/issues/40132.
3026 1946 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
3027 1947 continue;
@@ -3026,44 +1946,23 @@
3026 1946 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
3027 1947 continue;
3028 1948 }
3029 1949
3030 - if ( $is_root_style && $use_root_padding ) {
3031 - $root_variable_duplicates[] = substr( $css_property, $root_style_length );
1950 + if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
1951 + $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
3032 1952 }
3033 1953
3034 - /*
3035 - * Processes background image styles.
3036 - * If the value is a URL, it will be converted to a CSS `url()` value.
3037 - * For an uploaded image (images with a database ID), apply size and position
3038 - * defaults equal to those applied in block supports in lib/background.php.
3039 - */
3040 - if ( 'background-image' === $css_property ) {
3041 - $background_image_input = array();
3042 - if ( ! empty( $value ) ) {
3043 - $background_image_input['backgroundImage'] = $value;
1954 + // Look up protected properties, keyed by value path.
1955 + // Skip protected properties that are explicitly set to `null`.
1956 + if ( is_array( $value_path ) ) {
1957 + $path_string = implode( '.', $value_path );
1958 + if (
1959 + isset( static::PROTECTED_PROPERTIES[ $path_string ] ) &&
1960 + _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
1961 + ) {
1962 + continue;
3044 1963 }
3045 - $gradient_value = $styles['background']['gradient'] ?? null;
3046 - if ( ! empty( $gradient_value ) ) {
3047 - $background_image_input['gradient'] = $gradient_value;
3048 - }
3049 - if ( ! empty( $background_image_input ) ) {
3050 - $background_styles = gutenberg_style_engine_get_styles(
3051 - array( 'background' => $background_image_input )
3052 - );
3053 - $value = $background_styles['declarations'][ $css_property ] ?? null;
3054 - }
3055 1964 }
3056 - if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
3057 - if ( 'background-size' === $css_property ) {
3058 - $value = 'cover';
3059 - }
3060 - // If the background size is set to `contain` and no position is set, set the position to `center`.
3061 - if ( 'background-position' === $css_property ) {
3062 - $background_size = $styles['background']['backgroundSize'] ?? null;
3063 - $value = 'contain' === $background_size ? '50% 50%' : null;
3064 - }
3065 - }
3066 1965
3067 1966 // Skip if empty and not "0" or value represents array of longhand values.
3068 1967 $has_missing_value = empty( $value ) && ! is_numeric( $value );
3069 1968 if ( $has_missing_value || is_array( $value ) ) {
@@ -3077,22 +1976,12 @@
3077 1976 * if fluid typography has been activated and also
3078 1977 * whether the incoming value can be converted to a fluid value.
3079 1978 * Values that already have a clamp() function will not pass the test,
3080 1979 * and therefore the original $value will be returned.
3081 - * Pass the current theme_json settings to override any global settings.
3082 1980 */
3083 - $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ), $settings );
1981 + $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) );
3084 1982 }
3085 1983
3086 - if ( 'aspect-ratio' === $css_property ) {
3087 - // For aspect ratio to work, other dimensions rules must be unset.
3088 - // This ensures that a fixed height does not override the aspect ratio.
3089 - $declarations[] = array(
3090 - 'name' => 'min-height',
3091 - 'value' => 'unset',
3092 - );
3093 - }
3094 -
3095 1984 $declarations[] = array(
3096 1985 'name' => $css_property,
3097 1986 'value' => $value,
3098 1987 );
@@ -3118,9 +2007,8 @@
3118 2007 *
3119 2008 * @since 5.8.0
3120 2009 * @since 5.9.0 Added support for values of array type, which are returned as is.
3121 2010 * @since 6.1.0 Added the `$theme_json` parameter.
3122 - * @since 6.7.0 Added support for background image refs
3123 2011 *
3124 2012 * @param array $styles Styles subtree.
3125 2013 * @param array $path Which property to process.
3126 2014 * @param array $theme_json Theme JSON array.
@@ -3135,19 +2023,17 @@
3135 2023 return '';
3136 2024 }
3137 2025
3138 2026 /*
3139 - * Where the current value is an array with a 'ref' key pointing
3140 - * to a path, this converts that path into the value at that path.
2027 + * This converts references to a path to the value at that path
2028 + * where the values is an array with a "ref" key, pointing to a path.
3141 2029 * For example: { "ref": "style.color.background" } => "#fff".
3142 2030 */
3143 2031 if ( is_array( $value ) && isset( $value['ref'] ) ) {
3144 2032 $value_path = explode( '.', $value['ref'] );
3145 - $ref_value = _wp_array_get( $theme_json, $value_path, null );
3146 - // Background Image refs can refer to a string or an array containing a URL string.
3147 - $ref_value_url = $ref_value['url'] ?? null;
2033 + $ref_value = _wp_array_get( $theme_json, $value_path );
3148 2034 // Only use the ref value if we find anything.
3149 - if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
2035 + if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
3150 2036 $value = $ref_value;
3151 2037 }
3152 2038
3153 2039 if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
@@ -3167,8 +2053,12 @@
3167 2053 );
3168 2054 }
3169 2055 }
3170 2056
2057 + if ( is_array( $value ) ) {
2058 + return $value;
2059 + }
2060 +
3171 2061 return $value;
3172 2062 }
3173 2063
3174 2064 /**
@@ -3199,9 +2089,9 @@
3199 2089
3200 2090 // Top-level.
3201 2091 $nodes[] = array(
3202 2092 'path' => array( 'settings' ),
3203 - 'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
2093 + 'selector' => static::ROOT_BLOCK_SELECTOR,
3204 2094 );
3205 2095
3206 2096 // Calculate paths for blocks.
3207 2097 if ( ! isset( $theme_json['settings']['blocks'] ) ) {
@@ -3214,11 +2104,10 @@
3214 2104 $selector = $selectors[ $name ]['selector'];
3215 2105 }
3216 2106
3217 2107 $nodes[] = array(
3218 - 'path' => array( 'settings', 'blocks', $name ),
3219 - 'selector' => $selector,
3220 - 'selectors' => $selectors[ $name ]['selectors'] ?? array(),
2108 + 'path' => array( 'settings', 'blocks', $name ),
2109 + 'selector' => $selector,
3221 2110 );
3222 2111 }
3223 2112
3224 2113 return $nodes;
@@ -3243,14 +2132,11 @@
3243 2132 * @since 5.8.0
3244 2133 *
3245 2134 * @param array $theme_json The tree to extract style nodes from.
3246 2135 * @param array $selectors List of selectors per block.
3247 - * @param array $options An array of options to facilitate filtering style node generation
3248 - * The options currently supported are:
3249 - * - `include_block_style_variations` which includes CSS for block style variations.
3250 2136 * @return array An array of style nodes metadata.
3251 2137 */
3252 - protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
2138 + protected static function get_style_nodes( $theme_json, $selectors = array() ) {
3253 2139 $nodes = array();
3254 2140 if ( ! isset( $theme_json['styles'] ) ) {
3255 2141 return $nodes;
3256 2142 }
@@ -3292,13 +2178,9 @@
3292 2178 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3293 2179 return $nodes;
3294 2180 }
3295 2181
3296 - $block_options = $options;
3297 - if ( ! isset( $block_options['include_block_style_variations'] ) ) {
3298 - $block_options['include_block_style_variations'] = true;
3299 - }
3300 - $block_nodes = static::get_block_nodes( $theme_json, $selectors, $block_options );
2182 + $block_nodes = static::get_block_nodes( $theme_json, $selectors );
3301 2183 foreach ( $block_nodes as $block_node ) {
3302 2184 $nodes[] = $block_node;
3303 2185 }
3304 2186
@@ -3365,132 +2247,8 @@
3365 2247 return $declarations;
3366 2248 }
3367 2249
3368 2250 /**
3369 - * Updates button width declarations to use a calc() formula for percentage values.
3370 - *
3371 - * When a percentage width is set on the Button block via Global Styles, the
3372 - * resulting CSS needs to account for block gap spacing so that buttons tile
3373 - * correctly on a row (e.g. 4 buttons at 25% width all fit on one row).
3374 - *
3375 - * This mirrors the dynamic calc() formula applied at the block instance level
3376 - * in the button block's stylesheet (style.scss).
3377 - *
3378 - * @since 7.1.0
3379 - *
3380 - * @param array $feature_declarations The feature declarations keyed by selector.
3381 - * @param array $settings The theme.json settings.
3382 - * @return array The updated feature declarations.
3383 - */
3384 - private static function update_button_width_declarations( $feature_declarations, $settings ) {
3385 - if ( ! isset( $feature_declarations['.wp-block-button'] ) ) {
3386 - return $feature_declarations;
3387 - }
3388 -
3389 - foreach ( $feature_declarations['.wp-block-button'] as &$declaration ) {
3390 - if ( 'width' !== $declaration['name'] || ! isset( $declaration['value'] ) ) {
3391 - continue;
3392 - }
3393 -
3394 - $value = $declaration['value'];
3395 - $percentage = null;
3396 -
3397 - // Case 1: Direct percentage value e.g. "25%".
3398 - if ( is_string( $value ) && str_ends_with( $value, '%' ) ) {
3399 - $percentage = (float) $value;
3400 - }
3401 -
3402 - // Case 2: Preset CSS var e.g. "var(--wp--preset--dimension--50)".
3403 - if ( null === $percentage && is_string( $value ) && str_starts_with( $value, 'var(--wp--preset--dimension--' ) ) {
3404 - // Extract the slug from the var name.
3405 - $slug = substr( $value, strlen( 'var(--wp--preset--dimension--' ), -1 );
3406 -
3407 - /*
3408 - * Look up the preset size across all origins.
3409 - * Check block-level settings first (core/button), then top-level settings.
3410 - */
3411 - $dimension_sizes = ( $settings['blocks']['core/button']['dimensions']['dimensionSizes'] ?? array() )
3412 - + ( $settings['dimensions']['dimensionSizes'] ?? array() );
3413 - foreach ( $dimension_sizes as $origin_sizes ) {
3414 - if ( ! is_array( $origin_sizes ) ) {
3415 - continue;
3416 - }
3417 - foreach ( $origin_sizes as $preset ) {
3418 - if ( isset( $preset['slug'] ) && $slug === $preset['slug'] && isset( $preset['size'] ) ) {
3419 - $size = $preset['size'];
3420 - if ( is_string( $size ) && str_ends_with( $size, '%' ) ) {
3421 - $percentage = (float) $size;
3422 - }
3423 - break 2;
3424 - }
3425 - }
3426 - }
3427 - }
3428 -
3429 - if ( null === $percentage ) {
3430 - continue;
3431 - }
3432 -
3433 - /*
3434 - * Apply the same calc() formula as the block instance level (style.scss).
3435 - * The numeric percentage value is used as a unitless number:
3436 - * - Multiplied by 1% to get the percentage width.
3437 - * - Divided by 100 to calculate the gap adjustment proportion.
3438 - */
3439 - $declaration['value'] = sprintf(
3440 - 'calc(%s * 1%% - (var(--wp--style--block-gap, 0.5em) * (1 - %s / 100)))',
3441 - $percentage,
3442 - $percentage
3443 - );
3444 - }
3445 - unset( $declaration );
3446 -
3447 - return $feature_declarations;
3448 - }
3449 -
3450 - /**
3451 - * Updates the text indent selector for paragraph blocks based on the textIndent setting.
3452 - *
3453 - * The textIndent setting can be 'subsequent' (default), 'all', or false.
3454 - * When set to 'all', the selector should be '.wp-block-paragraph' instead of
3455 - * '.wp-block-paragraph + .wp-block-paragraph' to apply indent to all paragraphs.
3456 - *
3457 - * @since 7.0.0
3458 - *
3459 - * @param array $feature_declarations The feature declarations keyed by selector.
3460 - * @param array $settings The theme.json settings.
3461 - * @param string $block_name The block name being processed.
3462 - * @return array The updated feature declarations.
3463 - */
3464 - private static function update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ) {
3465 - if ( 'core/paragraph' !== $block_name ) {
3466 - return $feature_declarations;
3467 - }
3468 -
3469 - // Check block-level settings first, then fall back to global settings.
3470 - $block_settings = $settings['blocks']['core/paragraph'] ?? null;
3471 - $text_indent_setting = $block_settings['typography']['textIndent']
3472 - ?? $settings['typography']['textIndent']
3473 - ?? 'subsequent';
3474 -
3475 - if ( 'all' !== $text_indent_setting ) {
3476 - return $feature_declarations;
3477 - }
3478 -
3479 - // Look for the text indent selector and replace it.
3480 - $old_selector = '.wp-block-paragraph + .wp-block-paragraph';
3481 - $new_selector = '.wp-block-paragraph';
3482 -
3483 - if ( isset( $feature_declarations[ $old_selector ] ) ) {
3484 - $declarations = $feature_declarations[ $old_selector ];
3485 - unset( $feature_declarations[ $old_selector ] );
3486 - $feature_declarations[ $new_selector ] = $declarations;
3487 - }
3488 -
3489 - return $feature_declarations;
3490 - }
3491 -
3492 - /**
3493 2251 * An internal method to get the block nodes from a theme.json file.
3494 2252 *
3495 2253 * @since 6.1.0
3496 2254 *
@@ -3495,284 +2253,72 @@
3495 2253 * @since 6.1.0
3496 2254 *
3497 2255 * @param array $theme_json The theme.json converted to an array.
3498 2256 * @param array $selectors Optional list of selectors per block.
3499 - * @param array $options {
3500 - * Optional. An array of options for now used for internal purposes only (may change without notice).
3501 - *
3502 - * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3503 - * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
3504 - * }
3505 2257 * @return array The block nodes in theme.json.
3506 2258 */
3507 - private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
3508 - $nodes = array();
2259 + private static function get_block_nodes( $theme_json, $selectors = array() ) {
2260 + $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
2261 + $nodes = array();
2262 + if ( ! isset( $theme_json['styles'] ) ) {
2263 + return $nodes;
2264 + }
3509 2265
2266 + // Blocks.
3510 2267 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3511 2268 return $nodes;
3512 2269 }
3513 2270
3514 - $include_variations = $options['include_block_style_variations'] ?? false;
3515 - $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3516 - $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
3517 -
3518 - // If only node paths are to be returned, skip selector assignment.
3519 - if ( ! $include_node_paths_only ) {
3520 - $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
3521 - }
3522 -
3523 2271 foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
3524 - $node_path = array( 'styles', 'blocks', $name );
3525 - if ( $include_node_paths_only ) {
3526 - $variation_paths = array();
3527 - if ( $include_variations && isset( $node['variations'] ) ) {
3528 - foreach ( $node['variations'] as $variation => $variation_node ) {
3529 - $variation_paths[] = array(
3530 - 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3531 - );
3532 - }
3533 - }
3534 - $node = array(
3535 - 'path' => $node_path,
3536 - );
3537 - if ( ! empty( $variation_paths ) ) {
3538 - $node['variations'] = $variation_paths;
3539 - }
3540 - $nodes[] = $node;
3541 - } else {
3542 - $selector = null;
3543 - if ( isset( $selectors[ $name ]['selector'] ) ) {
3544 - $selector = $selectors[ $name ]['selector'];
3545 - }
2272 + $selector = null;
2273 + if ( isset( $selectors[ $name ]['selector'] ) ) {
2274 + $selector = $selectors[ $name ]['selector'];
2275 + }
3546 2276
3547 - $duotone_selector = null;
3548 - if ( isset( $selectors[ $name ]['duotone'] ) ) {
3549 - $duotone_selector = $selectors[ $name ]['duotone'];
3550 - }
2277 + $duotone_selector = null;
2278 + if ( isset( $selectors[ $name ]['duotone'] ) ) {
2279 + $duotone_selector = $selectors[ $name ]['duotone'];
2280 + }
3551 2281
3552 - $feature_selectors = null;
3553 - if ( isset( $selectors[ $name ]['selectors'] ) ) {
3554 - $feature_selectors = $selectors[ $name ]['selectors'];
3555 - }
2282 + $feature_selectors = null;
2283 + if ( isset( $selectors[ $name ]['selectors'] ) ) {
2284 + $feature_selectors = $selectors[ $name ]['selectors'];
2285 + }
3556 2286
3557 - $variation_selectors = array();
3558 -
3559 - if ( $include_variations && isset( $node['variations'] ) ) {
3560 - foreach ( $node['variations'] as $variation => $node ) {
3561 - $variation_selectors[] = array(
3562 - 'name' => $variation,
3563 - 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3564 - 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3565 - );
3566 - }
2287 + $variation_selectors = array();
2288 + if ( isset( $node['variations'] ) ) {
2289 + foreach ( $node['variations'] as $variation => $node ) {
2290 + $variation_selectors[] = array(
2291 + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
2292 + 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
2293 + );
3567 2294 }
2295 + }
3568 2296
3569 - $nodes[] = array(
3570 - 'name' => $name,
3571 - 'path' => $node_path,
3572 - 'selector' => $selector,
3573 - 'selectors' => $feature_selectors,
3574 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3575 - 'duotone' => $duotone_selector,
3576 - 'variations' => $variation_selectors,
3577 - 'css' => $selector,
3578 - );
2297 + $nodes[] = array(
2298 + 'name' => $name,
2299 + 'path' => array( 'styles', 'blocks', $name ),
2300 + 'selector' => $selector,
2301 + 'selectors' => $feature_selectors,
2302 + 'duotone' => $duotone_selector,
2303 + 'variations' => $variation_selectors,
2304 + );
3579 2305
3580 - // Responsive block nodes: emit one node per breakpoint that has styles.
3581 - // These are rendered immediately after the base block node so that
3582 - // the cascade order is: .block{} → @media{.block{}}
3583 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3584 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3585 - $nodes[] = array(
3586 - 'name' => $name,
3587 - 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3588 - 'media_query' => $responsive_media_queries[ $breakpoint ],
3589 - 'selector' => $selector,
3590 - 'selectors' => $feature_selectors,
3591 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3592 - 'variations' => $variation_selectors,
3593 - 'css' => $selector,
3594 - );
3595 - }
3596 - }
3597 -
3598 - // Handle any pseudo selectors for the block.
3599 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3600 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3601 - $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3602 - $has_responsive_pseudo = false;
3603 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3604 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3605 - $has_responsive_pseudo = true;
3606 - break;
3607 - }
3608 - }
3609 -
3610 - if ( ! $has_pseudo && ! $has_responsive_pseudo ) {
3611 - continue;
3612 - }
3613 -
3614 - /*
3615 - * Append the pseudo-selector to each feature selector so that
3616 - * get_feature_declarations_for_node generates CSS scoped to the
3617 - * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3618 - * default state (e.g. '.wp-block-button').
3619 - */
3620 - $pseudo_feature_selectors = array();
3621 - foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3622 - if ( is_array( $feature_selector ) ) {
3623 - $pseudo_feature_selectors[ $feature ] = array();
3624 - foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3625 - $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3626 - }
3627 - } else {
3628 - $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3629 - }
3630 - }
3631 -
3632 - if ( $has_pseudo ) {
3633 - $nodes[] = array(
3634 - 'name' => $name,
3635 - 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ),
3636 - 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3637 - 'selectors' => $pseudo_feature_selectors,
3638 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3639 - 'duotone' => $duotone_selector,
3640 - 'variations' => $variation_selectors,
3641 - 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3642 - );
3643 - }
3644 -
3645 - // Responsive pseudo nodes: emit one node per breakpoint that has
3646 - // this pseudo state, immediately after the default pseudo node.
3647 - // Cascade order: .block:hover{} → @media{.block:hover{}}
3648 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3649 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3650 - $nodes[] = array(
3651 - 'name' => $name,
3652 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3653 - 'media_query' => $responsive_media_queries[ $breakpoint ],
3654 - 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3655 - 'selectors' => $pseudo_feature_selectors,
3656 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3657 - 'variations' => $variation_selectors,
3658 - 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3659 - );
3660 - }
3661 - }
3662 - }
3663 - }
3664 -
3665 - // Handle custom states (e.g. '-current' for navigation).
3666 - if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3667 - foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3668 - if (
3669 - isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
3670 - isset( $selectors[ $name ]['states'][ $custom_state ] )
3671 - ) {
3672 - $custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
3673 - $nodes[] = array(
3674 - 'name' => $name,
3675 - 'path' => array( 'styles', 'blocks', $name, $custom_state ),
3676 - 'selector' => $custom_css_selector,
3677 - 'selectors' => $feature_selectors,
3678 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3679 - 'duotone' => $duotone_selector,
3680 - 'variations' => $variation_selectors,
3681 - 'css' => $custom_css_selector,
3682 - );
3683 -
3684 - // Sub-pseudo-selectors within the custom state.
3685 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3686 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) {
3687 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) {
3688 - $compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo );
3689 - $nodes[] = array(
3690 - 'name' => $name,
3691 - 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
3692 - 'selector' => $compound_css_selector,
3693 - 'selectors' => $feature_selectors,
3694 - 'elements' => $selectors[ $name ]['elements'] ?? array(),
3695 - 'duotone' => $duotone_selector,
3696 - 'variations' => $variation_selectors,
3697 - 'css' => $compound_css_selector,
3698 - );
3699 - }
3700 - }
3701 - }
3702 - }
3703 - }
3704 - }
3705 - }
3706 2306 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3707 2307 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3708 - $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3709 - if ( $include_node_paths_only ) {
3710 - $nodes[] = array(
3711 - 'path' => $element_path,
3712 - );
3713 - continue;
3714 - }
3715 -
3716 - $element_selector = $selectors[ $name ]['elements'][ $element ];
3717 -
3718 2308 $nodes[] = array(
3719 - 'path' => $element_path,
3720 - 'selector' => $element_selector,
2309 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
2310 + 'selector' => $selectors[ $name ]['elements'][ $element ],
3721 2311 );
3722 2312
3723 - // Responsive element nodes: one node per breakpoint that has
3724 - // styles for this element. Cascade: a{} → @media{a{}}
3725 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3726 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3727 - $nodes[] = array(
3728 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3729 - 'selector' => $element_selector,
3730 - 'media_query' => $responsive_media_queries[ $breakpoint ],
3731 - );
3732 - }
3733 - }
3734 -
3735 2313 // Handle any pseudo selectors for the element.
3736 2314 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3737 2315 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3738 - // Create element pseudo node if default or any responsive breakpoint has the pseudo.
3739 - $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] );
3740 - if ( ! $has_element_pseudo ) {
3741 - foreach ( array_keys( $responsive_media_queries ) as $bp ) {
3742 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3743 - $has_element_pseudo = true;
3744 - break;
3745 - }
3746 - }
3747 - }
3748 -
3749 - if ( $has_element_pseudo ) {
3750 - $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element );
3751 - if ( $include_node_paths_only ) {
3752 - $nodes[] = array(
3753 - 'path' => $element_pseudo_path,
3754 - );
3755 - continue;
3756 - }
3757 -
2316 + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3758 2317 $nodes[] = array(
3759 - 'path' => $element_pseudo_path,
3760 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
2318 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
2319 + 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
3761 2320 );
3762 -
3763 - // Responsive element pseudo nodes: one node per breakpoint
3764 - // that has this pseudo state for this element.
3765 - // Cascade: a:hover{} → @media{a:hover{}}
3766 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3767 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3768 - $nodes[] = array(
3769 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3770 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3771 - 'media_query' => $responsive_media_queries[ $breakpoint ],
3772 - );
3773 - }
3774 - }
3775 2321 }
3776 2322 }
3777 2323 }
3778 2324 }
@@ -3785,9 +2331,8 @@
3785 2331 /**
3786 2332 * Gets the CSS rules for a particular block from theme.json.
3787 2333 *
3788 2334 * @since 6.1.0
3789 - * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
3790 2335 *
3791 2336 * @param array $block_metadata Metadata about the block to get styles for.
3792 2337 *
3793 2338 * @return string Styles for the block.
@@ -3792,184 +2337,49 @@
3792 2337 *
3793 2338 * @return string Styles for the block.
3794 2339 */
3795 2340 public function get_styles_for_block( $block_metadata ) {
3796 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3797 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3798 - $selector = $block_metadata['selector'];
3799 - $settings = $this->theme_json['settings'] ?? null;
3800 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3801 - $media_query = $block_metadata['media_query'] ?? null;
3802 - $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null );
2341 + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
2342 + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
2343 + $selector = $block_metadata['selector'];
2344 + $settings = $this->theme_json['settings'] ?? null;
3803 2345
3804 2346 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3805 2347
3806 - // Update text indent selector for paragraph blocks based on the textIndent setting.
3807 - $block_name = $block_metadata['name'] ?? null;
3808 - $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
3809 - $block_elements = $block_metadata['elements'] ?? array();
3810 -
3811 - // Update button width declarations for percentage values to use calc() with block gap.
3812 - $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
3813 -
3814 2348 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3815 - // Responsive nodes (those with a media_query) do not process variations — variation responsive
3816 - // CSS is handled by the variation's own responsive nodes or the existing variation loop.
3817 - $style_variation_declarations = array();
3818 - $style_variation_custom_css = array();
3819 - $style_variation_responsive_css = array();
3820 - $style_variation_responsive_pseudo_css = array();
3821 - $style_variation_layout_metadata = array();
3822 - if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) {
2349 + $style_variation_declarations = array();
2350 + if ( ! empty( $block_metadata['variations'] ) ) {
3823 2351 foreach ( $block_metadata['variations'] as $style_variation ) {
3824 - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
2352 + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
2353 + $clean_style_variation_selector = trim( $style_variation['selector'] );
3825 2354
3826 2355 // Generate any feature/subfeature style declarations for the current style variation.
3827 2356 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3828 2357
3829 - // Update text indent selector for paragraph blocks based on the textIndent setting.
3830 - $variation_declarations = static::update_paragraph_text_indent_selector( $variation_declarations, $settings, $block_name );
3831 -
3832 - // Update button width declarations for percentage values to use calc() with block gap.
3833 - $variation_declarations = static::update_button_width_declarations( $variation_declarations, $settings );
3834 -
3835 2358 // Combine selectors with style variation's selector and add to overall style variation declarations.
3836 2359 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3837 - $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $current_selector );
2360 + // If current selector includes block classname, remove it but leave the whitespace in.
2361 + $shortened_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $current_selector );
3838 2362
2363 + // Prepend the variation selector to the current selector.
2364 + $split_selectors = explode( ',', $shortened_selector );
2365 + $updated_selectors = array_map(
2366 + static function ( $split_selector ) use ( $clean_style_variation_selector ) {
2367 + return $clean_style_variation_selector . $split_selector;
2368 + },
2369 + $split_selectors
2370 + );
2371 + $combined_selectors = implode( ',', $updated_selectors );
2372 +
3839 2373 // Add the new declarations to the overall results under the modified selector.
3840 2374 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3841 2375 }
2376 +
3842 2377 // Compute declarations for remaining styles not covered by feature level selectors.
3843 2378 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3844 -
3845 - // Process pseudo-selectors for this variation (e.g., :hover, :focus).
3846 - $block_name = $block_metadata['name'] ?? ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ? static::get_block_name_from_metadata_path( $block_metadata ) : null );
3847 - $variation_pseudo_declarations = $this->process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3848 - $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3849 -
3850 - // Store custom CSS for the style variation.
3851 - if ( isset( $style_variation_node['css'] ) ) {
3852 - $style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
3853 - }
3854 -
3855 - // Store variation metadata and node for layout styles generation.
3856 - // Only store if the variation has blockGap defined.
3857 - if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3858 - // Append block selector to the variation selector for proper targeting.
3859 - $variation_metadata_with_selector = $style_variation;
3860 - $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3861 - $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3862 - 'metadata' => $variation_metadata_with_selector,
3863 - 'node' => $style_variation_node,
3864 - );
3865 - }
3866 -
3867 - // Store responsive breakpoint CSS for the style variation.
3868 - // This includes both base properties and feature-level selectors.
3869 - $variation_responsive_css = '';
3870 - $variation_responsive_pseudo_css = '';
3871 -
3872 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3873 - if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3874 - continue;
3875 - }
3876 -
3877 - $breakpoint_node = $style_variation_node[ $breakpoint ];
3878 - $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3879 - // Process feature-level declarations for this breakpoint.
3880 - $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3881 - $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3882 - $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
3883 - foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) {
3884 - $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $feature_selector );
3885 -
3886 - $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl );
3887 - $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}';
3888 - }
3889 -
3890 - // Process base properties for this breakpoint.
3891 - $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
3892 - if ( ! empty( $breakpoint_declarations ) ) {
3893 - $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
3894 - $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
3895 - }
3896 -
3897 - $breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3898 - foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) {
3899 - if ( empty( $pseudo_declarations ) ) {
3900 - continue;
3901 - }
3902 - $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations );
3903 - $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}';
3904 - }
3905 -
3906 - // Process custom CSS for this breakpoint.
3907 - if ( isset( $breakpoint_node['css'] ) ) {
3908 - $breakpoint_custom_css = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] );
3909 - $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}';
3910 - }
3911 -
3912 - // Process blockGap responsive layout styles for this variation.
3913 - if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
3914 - $variation_layout_metadata = $style_variation;
3915 - $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
3916 - $variation_responsive_css .= $this->get_layout_styles(
3917 - $variation_layout_metadata,
3918 - array(
3919 - 'node' => $breakpoint_node,
3920 - 'media_query' => $breakpoint_media,
3921 - )
3922 - );
3923 - }
3924 -
3925 - // Process nested element styles for this breakpoint state.
3926 - if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) {
3927 - foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) {
3928 - if ( ! isset( $block_elements[ $element_name ] ) ) {
3929 - continue;
3930 - }
3931 -
3932 - $variation_element_selector = static::get_block_style_variation_feature_selector( $style_variation, $block_elements[ $element_name ] );
3933 -
3934 - $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json );
3935 - if ( ! empty( $element_declarations ) ) {
3936 - $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations );
3937 - $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}';
3938 - }
3939 -
3940 - if ( isset( $element_node['css'] ) ) {
3941 - $element_custom_css = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector );
3942 - $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}';
3943 - }
3944 -
3945 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
3946 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
3947 - if ( ! isset( $element_node[ $pseudo_selector ] ) ) {
3948 - continue;
3949 - }
3950 -
3951 - $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json );
3952 - if ( empty( $pseudo_declarations ) ) {
3953 - continue;
3954 - }
3955 -
3956 - $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
3957 - $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
3958 - }
3959 - }
3960 - }
3961 - }
3962 - }
3963 -
3964 - if ( ! empty( $variation_responsive_css ) ) {
3965 - $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css;
3966 - }
3967 - if ( ! empty( $variation_responsive_pseudo_css ) ) {
3968 - $style_variation_responsive_pseudo_css[ $style_variation['selector'] ] = $variation_responsive_pseudo_css;
3969 - }
3970 2379 }
3971 2380 }
2381 +
3972 2382 /*
3973 2383 * Get a reference to element name from path.
3974 2384 * $block_metadata['path'] = array( 'styles','elements','link' );
3975 2385 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
@@ -3980,9 +2390,9 @@
3980 2390 $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
3981 2391
3982 2392 $element_pseudo_allowed = array();
3983 2393
3984 - if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
2394 + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
3985 2395 $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
3986 2396 }
3987 2397
3988 2398 /*
@@ -3992,18 +2402,14 @@
3992 2402 $pseudo_matches = array_values(
3993 2403 array_filter(
3994 2404 $element_pseudo_allowed,
3995 2405 static function ( $pseudo_selector ) use ( $selector ) {
3996 - /*
3997 - * Check if the pseudo selector is in the current selector,
3998 - * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible).
3999 - */
4000 - return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1;
2406 + return str_contains( $selector, $pseudo_selector );
4001 2407 }
4002 2408 )
4003 2409 );
4004 2410
4005 - $pseudo_selector = $pseudo_matches[0] ?? null;
2411 + $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
4006 2412
4007 2413 /*
4008 2414 * If the current selector is a pseudo selector that's defined in the allow list for the current
4009 2415 * element then compute the style properties for it.
@@ -4014,13 +2420,8 @@
4014 2420 && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
4015 2421 ) {
4016 2422 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
4017 2423 } else {
4018 - /*
4019 - * For block pseudo-selector nodes (e.g. ':hover'), $node has already had any
4020 - * feature-selector properties (e.g. writingMode) removed by get_feature_declarations_for_node,
4021 - * so those properties are not output twice.
4022 - */
4023 2424 $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
4024 2425 }
4025 2426
4026 2427 $block_rules = '';
@@ -4025,18 +2426,12 @@
4025 2426
4026 2427 $block_rules = '';
4027 2428
4028 2429 /*
4029 - * 1. Bespoke declaration modifiers:
4030 - * - 'filter': Separate the declarations that use the general selector
2430 + * 1. Separate the declarations that use the general selector
4031 2431 * from the ones using the duotone selector.
4032 - * - 'background|background-image': set the html min-height to 100%
4033 - * to ensure the background covers the entire viewport.
4034 - *
4035 2432 */
4036 - $declarations_duotone = array();
4037 - $should_set_root_min_height = false;
4038 -
2433 + $declarations_duotone = array();
4039 2434 foreach ( $declarations as $index => $declaration ) {
4040 2435 if ( 'filter' === $declaration['name'] ) {
4041 2436 /*
4042 2437 * 'unset' filters happen when a filter is unset
@@ -4051,59 +2446,17 @@
4051 2446 $declarations_duotone[] = $declaration;
4052 2447 }
4053 2448 unset( $declarations[ $index ] );
4054 2449 }
4055 -
4056 - if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
4057 - $should_set_root_min_height = true;
4058 - }
4059 2450 }
4060 2451
4061 - /*
4062 - * If root styles has a background-image or a background (gradient) set,
4063 - * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
4064 - * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
4065 - * and matches the behavior of the site editor.
4066 - */
4067 - if ( $should_set_root_min_height ) {
4068 - $block_rules .= static::to_ruleset(
4069 - 'html',
4070 - array(
4071 - array(
4072 - 'name' => 'min-height',
4073 - 'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
4074 - ),
4075 - )
4076 - );
4077 - }
4078 -
4079 2452 // Update declarations if there are separators with only background color defined.
4080 2453 if ( '.wp-block-separator' === $selector ) {
4081 2454 $declarations = static::update_separator_declarations( $declarations );
4082 2455 }
4083 2456
4084 - /*
4085 - * Root selector (body) styles should not be wrapped in `:root where()` to keep
4086 - * specificity at (0,0,1) and maintain backwards compatibility.
4087 - *
4088 - * Top-level element styles using element-only specificity selectors should
4089 - * not get wrapped in `:root :where()` to maintain backwards compatibility.
4090 - *
4091 - * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
4092 - * still need to be wrapped in `:root :where` to cap specificity for nested
4093 - * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
4094 - */
4095 - $element_only_selector = $is_root_selector || (
4096 - $current_element &&
4097 - isset( static::ELEMENTS[ $current_element ] ) &&
4098 - // buttons, captions etc. still need `:root :where()` as they are class based selectors.
4099 - ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
4100 - static::ELEMENTS[ $current_element ] === $selector
4101 - );
4102 -
4103 2457 // 2. Generate and append the rules that use the general selector.
4104 - $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
4105 - $block_rules .= static::to_ruleset( $general_selector, $declarations );
2458 + $block_rules .= static::to_ruleset( $selector, $declarations );
4106 2459
4107 2460 // 3. Generate and append the rules that use the duotone selector.
4108 2461 if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
4109 2462 $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
@@ -4110,9 +2463,9 @@
4110 2463 }
4111 2464
4112 2465 // 4. Generate Layout block gap styles.
4113 2466 if (
4114 - ! $is_root_selector &&
2467 + static::ROOT_BLOCK_SELECTOR !== $selector &&
4115 2468 ! empty( $block_metadata['name'] )
4116 2469 ) {
4117 2470 $block_rules .= $this->get_layout_styles( $block_metadata );
4118 2471 }
@@ -4118,52 +2471,16 @@
4118 2471 }
4119 2472
4120 2473 // 5. Generate and append the feature level rulesets.
4121 2474 foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
4122 - $block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
2475 + $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations );
4123 2476 }
4124 2477
4125 2478 // 6. Generate and append the style variation rulesets.
4126 2479 foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
4127 - $block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
4128 - if ( isset( $style_variation_layout_metadata[ $style_variation_selector ] ) ) {
4129 - $variation_data = $style_variation_layout_metadata[ $style_variation_selector ];
4130 - $block_rules .= $this->get_layout_styles( $variation_data['metadata'], array( 'node' => $variation_data['node'] ) );
4131 - }
4132 - if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
4133 - $block_rules .= $style_variation_custom_css[ $style_variation_selector ];
4134 - }
4135 - if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) {
4136 - $block_rules .= $style_variation_responsive_css[ $style_variation_selector ];
4137 - }
2480 + $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations );
4138 2481 }
4139 - /*
4140 - * Responsive pseudo styles must be output after default pseudo styles
4141 - * so viewport state styles win in the cascade.
4142 - */
4143 - foreach ( $style_variation_responsive_pseudo_css as $responsive_pseudo_css ) {
4144 - $block_rules .= $responsive_pseudo_css;
4145 - }
4146 2482
4147 - // Compute selector for block custom CSS.
4148 - $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
4149 - if ( is_array( $css_feature_selector ) ) {
4150 - $css_feature_selector = $css_feature_selector['root'] ?? null;
4151 - }
4152 - $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
4153 -
4154 - // 7. Generate and append any custom CSS rules.
4155 - if ( isset( $node['css'] ) && ! $is_root_selector ) {
4156 - $block_rules .= $this->process_blocks_custom_css( $node['css'], $css_selector );
4157 - }
4158 -
4159 - // 8. Wrap the entire block output in a media query if this is a responsive node.
4160 - // Responsive nodes are created by get_block_nodes() for each breakpoint and carry
4161 - // a 'media_query' key.
4162 - if ( $media_query && ! empty( $block_rules ) ) {
4163 - $block_rules = $media_query . '{' . $block_rules . '}';
4164 - }
4165 -
4166 2483 return $block_rules;
4167 2484 }
4168 2485
4169 2486 /**
@@ -4169,42 +2486,42 @@
4169 2486 /**
4170 2487 * Outputs the CSS for layout rules on the root.
4171 2488 *
4172 2489 * @since 6.1.0
4173 - * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties.
4174 2490 *
4175 - * @param string $selector The root node selector.
2491 + * @param string $selector The root node selector.
4176 2492 * @param array $block_metadata The metadata for the root block.
4177 - * @param array $options Optional. An array of options. Default empty array.
4178 2493 * @return string The additional root rules CSS.
4179 2494 */
4180 - public function get_root_layout_rules( $selector, $block_metadata, $options = array() ) {
2495 + public function get_root_layout_rules( $selector, $block_metadata ) {
4181 2496 $css = '';
4182 2497 $settings = $this->theme_json['settings'] ?? array();
4183 2498 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
4184 2499
4185 2500 /*
2501 + * Reset default browser margin on the root body element.
2502 + * This is set on the root selector **before** generating the ruleset
2503 + * from the `theme.json`. This is to ensure that if the `theme.json` declares
2504 + * `margin` in its `spacing` declaration for the `body` element then these
2505 + * user-generated values take precedence in the CSS cascade.
2506 + * @link https://github.com/WordPress/gutenberg/issues/36147.
2507 + */
2508 + $css .= 'body { margin: 0;';
2509 +
2510 + /*
4186 2511 * If there are content and wide widths in theme.json, output them
4187 2512 * as custom properties on the body element so all blocks can use them.
4188 2513 */
4189 2514 if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
4190 - $content_size = $settings['layout']['contentSize'] ?? $settings['layout']['wideSize'];
2515 + $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
4191 2516 $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
4192 - $wide_size = $settings['layout']['wideSize'] ?? $settings['layout']['contentSize'];
2517 + $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
4193 2518 $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
4194 - $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
4195 - $css .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
2519 + $css .= '--wp--style--global--content-size: ' . $content_size . ';';
2520 + $css .= '--wp--style--global--wide-size: ' . $wide_size . ';';
4196 2521 }
4197 2522
4198 - /*
4199 - * Reset default browser margin on the body element.
4200 - * This is set on the body selector **before** generating the ruleset
4201 - * from the `theme.json`. This is to ensure that if the `theme.json` declares
4202 - * `margin` in its `spacing` declaration for the `body` element then these
4203 - * user-generated values take precedence in the CSS cascade.
4204 - * @link https://github.com/WordPress/gutenberg/issues/36147.
4205 - */
4206 - $css .= ':where(body) { margin: 0; }';
2523 + $css .= '}';
4207 2524
4208 2525 if ( $use_root_padding ) {
4209 2526 // Top and bottom padding are applied to the outer block container.
4210 2527 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
@@ -4209,35 +2526,36 @@
4209 2526 // Top and bottom padding are applied to the outer block container.
4210 2527 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
4211 2528 // Right and left padding are applied to the first container with `.has-global-padding` class.
4212 2529 $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2530 + // Nested containers with `.has-global-padding` class do not get padding.
2531 + $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }';
4213 2532 // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
4214 2533 $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); }';
4215 - // 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.
4216 - $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
4217 - // Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
4218 - $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; }';
2534 + // The above rule is negated for alignfull children of nested containers.
2535 + $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }';
2536 + // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
2537 + $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2538 + // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
2539 + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }';
4219 2540 }
4220 2541
4221 - // Skip outputting alignment styles when base_layout_styles is enabled.
4222 - // These styles target .wp-site-blocks which is only used by block themes.
4223 - if ( empty( $options['base_layout_styles'] ) ) {
4224 - $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
4225 - $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
4226 - $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
4227 - }
2542 + $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
2543 + $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
2544 + $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
4228 2545
4229 - // Block gap styles will be output unless explicitly set to `null`.
4230 - if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
2546 + $block_gap_value = $this->theme_json['styles']['spacing']['blockGap'] ?? '0.5em';
2547 + $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] );
2548 + if ( $has_block_gap_support ) {
4231 2549 $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
4232 2550 $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
4233 - $css .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
4234 - $css .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
2551 + $css .= ':where(.wp-site-blocks) > :first-child:first-child { margin-block-start: 0; }';
2552 + $css .= ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }';
4235 2553
4236 2554 // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
4237 - $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
2555 + $css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
4238 2556 }
4239 - $css .= $this->get_layout_styles( $block_metadata, $options );
2557 + $css .= $this->get_layout_styles( $block_metadata );
4240 2558
4241 2559 return $css;
4242 2560 }
4243 2561
@@ -4285,12 +2603,10 @@
4285 2603 * Merges new incoming data.
4286 2604 *
4287 2605 * @since 5.8.0
4288 2606 * @since 5.9.0 Duotone preset also has origins.
4289 - * @since 6.6.0 Use the spacingScale keyed by origin, and re-generate the
4290 - * spacingSizes from spacingScale.
4291 2607 *
4292 - * @param WP_Theme_JSON_Gutenberg $incoming Data to merge.
2608 + * @param WP_Theme_JSON $incoming Data to merge.
4293 2609 */
4294 2610 public function merge( $incoming ) {
4295 2611 $incoming_data = $incoming->get_raw_data();
4296 2612 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
@@ -4295,42 +2611,8 @@
4295 2611 $incoming_data = $incoming->get_raw_data();
4296 2612 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
4297 2613
4298 2614 /*
4299 - * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
4300 - * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
4301 - * we can allow partial spacingScale data to inherit missing data from earlier layers when
4302 - * computing the spacing sizes.
4303 - *
4304 - * This happens before the presets are merged to ensure that default spacing sizes can be
4305 - * removed from the theme origin if $prevent_override is true.
4306 - */
4307 - $flattened_spacing_scale = array();
4308 - foreach ( static::VALID_ORIGINS as $origin ) {
4309 - $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
4310 -
4311 - // Apply the base spacing scale to the current layer.
4312 - $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() );
4313 - $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
4314 -
4315 - $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
4316 - if ( ! isset( $spacing_scale ) ) {
4317 - continue;
4318 - }
4319 -
4320 - // Allow partial scale settings by merging with lower layers.
4321 - $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
4322 -
4323 - // Generate and merge the scales for this layer.
4324 - $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
4325 - $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() );
4326 - $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale );
4327 - $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
4328 -
4329 - _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
4330 - }
4331 -
4332 - /*
4333 2615 * The array_replace_recursive algorithm merges at the leaf level,
4334 2616 * but we don't want leaf arrays to be merged, so we overwrite it.
4335 2617 *
4336 2618 * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
@@ -4364,21 +2646,14 @@
4364 2646 _wp_array_set( $this->theme_json, $path, $content );
4365 2647 }
4366 2648
4367 2649 // Replace the presets.
4368 - foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4369 - $prevent_override = $preset_metadata['prevent_override'];
4370 - if ( is_array( $prevent_override ) ) {
4371 - $global_path = array_merge( array( 'settings' ), $prevent_override );
4372 - $global_value = _wp_array_get( $this->theme_json, $global_path, null );
2650 + foreach ( static::PRESETS_METADATA as $preset ) {
2651 + $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
4373 2652
4374 - $node_level_path = array_merge( $node['path'], $prevent_override );
4375 - $prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value );
4376 - }
4377 -
4378 2653 foreach ( static::VALID_ORIGINS as $origin ) {
4379 2654 $base_path = $node['path'];
4380 - foreach ( $preset_metadata['path'] as $leaf ) {
2655 + foreach ( $preset['path'] as $leaf ) {
4381 2656 $base_path[] = $leaf;
4382 2657 }
4383 2658
4384 2659 $path = $base_path;
@@ -4388,10 +2663,9 @@
4388 2663 if ( ! isset( $content ) ) {
4389 2664 continue;
4390 2665 }
4391 2666
4392 - // Set names for theme presets based on the slug if they are not set and can use default names.
4393 - if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
2667 + if ( 'theme' === $origin && $preset['use_default_names'] ) {
4394 2668 foreach ( $content as $key => $item ) {
4395 2669 if ( ! isset( $item['name'] ) ) {
4396 2670 $name = static::get_name_from_defaults( $item['slug'], $base_path );
4397 2671 if ( null !== $name ) {
@@ -4400,49 +2674,24 @@
4400 2674 }
4401 2675 }
4402 2676 }
4403 2677
4404 - // Filter out default slugs from theme presets when defaults should not be overridden.
4405 - if ( 'theme' === $origin && $prevent_override ) {
4406 - $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
4407 - $preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
4408 - $preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
4409 - $preset_slugs = array_merge_recursive( $preset_global, $preset_node );
2678 + if (
2679 + ( 'theme' !== $origin ) ||
2680 + ( 'theme' === $origin && $override_preset )
2681 + ) {
2682 + _wp_array_set( $this->theme_json, $path, $content );
2683 + } else {
2684 + $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
2685 + $slugs = array_merge_recursive( $slugs_global, $slugs_node );
4410 2686
4411 - $content = static::filter_slugs( $content, $preset_slugs );
2687 + $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
2688 + $content = static::filter_slugs( $content, $slugs_for_preset );
2689 + _wp_array_set( $this->theme_json, $path, $content );
4412 2690 }
4413 -
4414 - _wp_array_set( $this->theme_json, $path, $content );
4415 2691 }
4416 2692 }
4417 2693 }
4418 -
4419 - /*
4420 - * Style values are merged at the leaf level, however
4421 - * some values provide exceptions, namely style values that are
4422 - * objects and represent unique definitions for the style.
4423 - */
4424 - $style_nodes = static::get_block_nodes(
4425 - $this->theme_json,
4426 - array(),
4427 - array( 'include_node_paths_only' => true )
4428 - );
4429 -
4430 - // Add top-level styles.
4431 - $style_nodes[] = array( 'path' => array( 'styles' ) );
4432 -
4433 - foreach ( $style_nodes as $style_node ) {
4434 - $path = $style_node['path'];
4435 - /*
4436 - * Background image styles should be replaced, not merged,
4437 - * as they themselves are specific object definitions for the style.
4438 - */
4439 - $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
4440 - $content = _wp_array_get( $incoming_data, $background_image_path, null );
4441 - if ( isset( $content ) ) {
4442 - _wp_array_set( $this->theme_json, $background_image_path, $content );
4443 - }
4444 - }
4445 2694 }
4446 2695
4447 2696 /**
4448 2697 * Converts all filter (duotone) presets into SVGs.
@@ -4521,15 +2770,13 @@
4521 2770 }
4522 2771
4523 2772 return true;
4524 2773 }
4525 -
4526 - return false;
4527 2774 }
4528 2775
4529 2776 /**
4530 2777 * Returns the default slugs for all the presets in an associative array
4531 - * whose keys are the preset paths and the leaves is the list of slugs.
2778 + * whose keys are the preset paths and the leafs is the list of slugs.
4532 2779 *
4533 2780 * For example:
4534 2781 *
4535 2782 * array(
@@ -4624,38 +2871,31 @@
4624 2871 /**
4625 2872 * Removes insecure data from theme.json.
4626 2873 *
4627 2874 * @since 5.9.0
4628 - * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
4629 2875 *
4630 - * @param array $theme_json Structure to sanitize.
4631 - * @param string $origin Optional. What source of data this object represents.
4632 - * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
2876 + * @param array $theme_json Structure to sanitize.
4633 2877 * @return array Sanitized structure.
4634 2878 */
4635 - public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4636 - if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
4637 - $origin = 'theme';
4638 - }
4639 -
2879 + public static function remove_insecure_properties( $theme_json ) {
4640 2880 $sanitized = array();
4641 2881
4642 - $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
4643 - if ( isset( $theme_json['styles'] ) ) {
4644 - $theme_json['styles'] = gutenberg_resolve_style_state_aliases( $theme_json['styles'] );
4645 - }
2882 + $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json );
4646 2883
4647 - $blocks_metadata = static::get_blocks_metadata();
4648 - $valid_block_names = array_keys( $blocks_metadata );
2884 + $valid_block_names = array_keys( static::get_blocks_metadata() );
4649 2885 $valid_element_names = array_keys( static::ELEMENTS );
4650 - $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
2886 + $valid_variations = array();
2887 + foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) {
2888 + if ( ! isset( $block_meta['styleVariations'] ) ) {
2889 + continue;
2890 + }
2891 + $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
2892 + }
4651 2893
4652 2894 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4653 2895
4654 - $blocks_metadata = static::get_blocks_metadata();
4655 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4656 - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4657 - $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
2896 + $blocks_metadata = static::get_blocks_metadata();
2897 + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata );
4658 2898
4659 2899 foreach ( $style_nodes as $metadata ) {
4660 2900 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4661 2901 if ( empty( $input ) ) {
@@ -4661,12 +2901,8 @@
4661 2901 if ( empty( $input ) ) {
4662 2902 continue;
4663 2903 }
4664 2904
4665 - $block_name = in_array( 'blocks', $metadata['path'], true )
4666 - ? static::get_block_name_from_metadata_path( $metadata )
4667 - : null;
4668 -
4669 2905 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
4670 2906 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
4671 2907 $output = $input;
4672 2908 } else {
@@ -4690,36 +2926,8 @@
4690 2926 }
4691 2927 }
4692 2928 }
4693 2929
4694 - // Re-add and process responsive breakpoint styles.
4695 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4696 - if ( isset( $input[ $breakpoint ] ) ) {
4697 - $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4698 -
4699 - if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4700 - $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4701 - }
4702 -
4703 - if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4704 - $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4705 - }
4706 -
4707 - if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4708 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4709 - if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) {
4710 - $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] );
4711 - }
4712 - }
4713 - }
4714 -
4715 - // Responsive custom CSS is allowed for users with 'edit_css' capability.
4716 - if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4717 - $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css'];
4718 - }
4719 - }
4720 - }
4721 -
4722 2930 if ( ! empty( $output ) ) {
4723 2931 _wp_array_set( $sanitized, $metadata['path'], $output );
4724 2932 }
4725 2933
@@ -4730,45 +2938,8 @@
4730 2938 continue;
4731 2939 }
4732 2940
4733 2941 $variation_output = static::remove_insecure_styles( $variation_input );
4734 -
4735 - if ( isset( $variation_input['blocks'] ) ) {
4736 - $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'], $responsive_media_queries );
4737 - }
4738 -
4739 - if ( isset( $variation_input['elements'] ) ) {
4740 - $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'], $responsive_media_queries );
4741 - }
4742 -
4743 - // Re-add and process responsive breakpoint styles for variations.
4744 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4745 - if ( isset( $variation_input[ $breakpoint ] ) ) {
4746 - $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4747 -
4748 - if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4749 - $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4750 - }
4751 -
4752 - if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4753 - $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4754 - }
4755 -
4756 - if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4757 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4758 - if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4759 - $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] );
4760 - }
4761 - }
4762 - }
4763 -
4764 - // Responsive custom CSS is allowed for users with 'edit_css' capability.
4765 - if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4766 - $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css'];
4767 - }
4768 - }
4769 - }
4770 -
4771 2942 if ( ! empty( $variation_output ) ) {
4772 2943 _wp_array_set( $sanitized, $variation['path'], $variation_output );
4773 2944 }
4774 2945 }
@@ -4781,9 +2952,9 @@
4781 2952 if ( empty( $input ) ) {
4782 2953 continue;
4783 2954 }
4784 2955
4785 - $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
2956 + $output = static::remove_insecure_settings( $input );
4786 2957 if ( ! empty( $output ) ) {
4787 2958 _wp_array_set( $sanitized, $metadata['path'], $output );
4788 2959 }
4789 2960 }
@@ -4803,145 +2974,17 @@
4803 2974 return $theme_json;
4804 2975 }
4805 2976
4806 2977 /**
4807 - * Remove insecure element styles within a variation or block.
4808 - *
4809 - * When responsive media queries are provided, nested responsive state styles
4810 - * for those media-query keys are re-added after the base sanitization pass.
4811 - *
4812 - * @since 6.8.0
4813 - *
4814 - * @param array $elements The elements to process.
4815 - * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4816 - * viewport states. Default null.
4817 - * @return array The sanitized elements styles.
4818 - */
4819 - protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4820 - $sanitized = array();
4821 - $valid_element_names = array_keys( static::ELEMENTS );
4822 -
4823 - foreach ( $valid_element_names as $element_name ) {
4824 - $element_input = $elements[ $element_name ] ?? null;
4825 - if ( $element_input ) {
4826 - $element_output = static::remove_insecure_styles( $element_input );
4827 -
4828 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4829 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4830 - if ( isset( $element_input[ $pseudo_selector ] ) ) {
4831 - $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
4832 - }
4833 - }
4834 - }
4835 -
4836 - if ( null !== $responsive_media_queries ) {
4837 - // Re-add and process responsive breakpoint styles for elements.
4838 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4839 - if ( isset( $element_input[ $breakpoint ] ) ) {
4840 - $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4841 -
4842 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4843 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4844 - if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4845 - $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4846 - }
4847 - }
4848 - }
4849 - }
4850 - }
4851 - }
4852 -
4853 - $sanitized[ $element_name ] = $element_output;
4854 - }
4855 - }
4856 - return $sanitized;
4857 - }
4858 -
4859 - /**
4860 - * Remove insecure styles from inner blocks and their elements.
4861 - *
4862 - * When responsive media queries are provided, nested responsive state styles
4863 - * for those media-query keys are re-added after the base sanitization pass.
4864 - *
4865 - * @since 6.8.0
4866 - *
4867 - * @param array $blocks The block styles to process.
4868 - * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4869 - * viewport states. Default null.
4870 - * @return array Sanitized block type styles.
4871 - */
4872 - protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4873 - $sanitized = array();
4874 - foreach ( $blocks as $block_type => $block_input ) {
4875 - $block_output = static::remove_insecure_styles( $block_input );
4876 -
4877 - if ( isset( $block_input['elements'] ) ) {
4878 - $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'], $responsive_media_queries );
4879 - }
4880 -
4881 - if ( null !== $responsive_media_queries ) {
4882 - // Re-add and process responsive breakpoint styles for inner blocks.
4883 - foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4884 - if ( isset( $block_input[ $breakpoint ] ) ) {
4885 - $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4886 -
4887 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4888 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4889 - if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4890 - $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
4891 - }
4892 - }
4893 - }
4894 - }
4895 - }
4896 - }
4897 -
4898 - $sanitized[ $block_type ] = $block_output;
4899 - }
4900 - return $sanitized;
4901 - }
4902 -
4903 - /**
4904 - * Preserves valid typed settings from input to output based on type markers in schema.
4905 - *
4906 - * Recursively iterates through the schema and validates/preserves settings
4907 - * that have type markers (e.g., boolean) in VALID_SETTINGS.
4908 - *
4909 - * @since 7.0.0
4910 - *
4911 - * @param array $input Input settings to process.
4912 - * @param array $output Output settings array (passed by reference).
4913 - * @param array $schema Schema to validate against (typically VALID_SETTINGS).
4914 - * @param array<string|int> $path Current path in the schema (for recursive calls).
4915 - */
4916 - private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) {
4917 - foreach ( $schema as $key => $schema_value ) {
4918 - $current_path = array_merge( $path, array( $key ) );
4919 -
4920 - // Validate boolean type markers.
4921 - if ( is_bool( $schema_value ) ) {
4922 - $value = _wp_array_get( $input, $current_path, null );
4923 - if ( null !== $value && is_bool( $value ) ) {
4924 - _wp_array_set( $output, $current_path, $value ); // Preserve boolean value.
4925 - }
4926 - } elseif ( is_array( $schema_value ) ) {
4927 - self::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure.
4928 - }
4929 - }
4930 - }
4931 -
4932 - /**
4933 2978 * Processes a setting node and returns the same node
4934 2979 * without the insecure settings.
4935 2980 *
4936 2981 * @since 5.9.0
4937 2982 *
4938 - * @param array $input Node to process.
4939 - * @param bool $allow_viewport Whether to preserve and sanitize top-level
4940 - * viewport settings.
2983 + * @param array $input Node to process.
4941 2984 * @return array
4942 2985 */
4943 - protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
2986 + protected static function remove_insecure_settings( $input ) {
4944 2987 $output = array();
4945 2988 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4946 2989 foreach ( static::VALID_ORIGINS as $origin ) {
4947 2990 $path_with_origin = $preset_metadata['path'];
@@ -4989,15 +3032,8 @@
4989 3032
4990 3033 // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
4991 3034 static::remove_indirect_properties( $input, $output );
4992 3035
4993 - // Preserve all valid settings that have type markers in VALID_SETTINGS.
4994 - self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4995 -
4996 - if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
4997 - $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
4998 - }
4999 -
5000 3036 return $output;
5001 3037 }
5002 3038
5003 3039 /**
@@ -5098,24 +3134,39 @@
5098 3134 );
5099 3135
5100 3136 // Deprecated theme supports.
5101 3137 if ( isset( $settings['disableCustomColors'] ) ) {
3138 + if ( ! isset( $theme_settings['settings']['color'] ) ) {
3139 + $theme_settings['settings']['color'] = array();
3140 + }
5102 3141 $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
5103 3142 }
5104 3143
5105 3144 if ( isset( $settings['disableCustomGradients'] ) ) {
3145 + if ( ! isset( $theme_settings['settings']['color'] ) ) {
3146 + $theme_settings['settings']['color'] = array();
3147 + }
5106 3148 $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
5107 3149 }
5108 3150
5109 3151 if ( isset( $settings['disableCustomFontSizes'] ) ) {
3152 + if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3153 + $theme_settings['settings']['typography'] = array();
3154 + }
5110 3155 $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
5111 3156 }
5112 3157
5113 3158 if ( isset( $settings['enableCustomLineHeight'] ) ) {
3159 + if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3160 + $theme_settings['settings']['typography'] = array();
3161 + }
5114 3162 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
5115 3163 }
5116 3164
5117 3165 if ( isset( $settings['enableCustomUnits'] ) ) {
3166 + if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3167 + $theme_settings['settings']['spacing'] = array();
3168 + }
5118 3169 $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
5119 3170 array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
5120 3171 $settings['enableCustomUnits'];
5121 3172 }
@@ -5120,12 +3171,18 @@
5120 3171 $settings['enableCustomUnits'];
5121 3172 }
5122 3173
5123 3174 if ( isset( $settings['colors'] ) ) {
3175 + if ( ! isset( $theme_settings['settings']['color'] ) ) {
3176 + $theme_settings['settings']['color'] = array();
3177 + }
5124 3178 $theme_settings['settings']['color']['palette'] = $settings['colors'];
5125 3179 }
5126 3180
5127 3181 if ( isset( $settings['gradients'] ) ) {
3182 + if ( ! isset( $theme_settings['settings']['color'] ) ) {
3183 + $theme_settings['settings']['color'] = array();
3184 + }
5128 3185 $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
5129 3186 }
5130 3187
5131 3188 if ( isset( $settings['fontSizes'] ) ) {
@@ -5135,19 +3192,21 @@
5135 3192 if ( is_numeric( $font_size['size'] ) ) {
5136 3193 $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
5137 3194 }
5138 3195 }
3196 + if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3197 + $theme_settings['settings']['typography'] = array();
3198 + }
5139 3199 $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
5140 3200 }
5141 3201
5142 3202 if ( isset( $settings['enableCustomSpacing'] ) ) {
3203 + if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3204 + $theme_settings['settings']['spacing'] = array();
3205 + }
5143 3206 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
5144 3207 }
5145 3208
5146 - if ( isset( $settings['spacingSizes'] ) ) {
5147 - $theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes'];
5148 - }
5149 -
5150 3209 return $theme_settings;
5151 3210 }
5152 3211
5153 3212 /**
@@ -5311,19 +3370,14 @@
5311 3370 /**
5312 3371 * Sets the spacingSizes array based on the spacingScale values from theme.json.
5313 3372 *
5314 3373 * @since 6.1.0
5315 - * @deprecated 6.6.0 No longer used as the spacingSizes are automatically
5316 - * generated in the constructor and merge methods instead
5317 - * of manually after instantiation.
5318 3374 *
5319 3375 * @return null|void
5320 3376 */
5321 3377 public function set_spacing_sizes() {
5322 - _deprecated_function( __METHOD__, '6.6.0' );
3378 + $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array();
5323 3379
5324 - $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale']['default'] ?? array();
5325 -
5326 3380 // Gutenberg didn't have the 1st isset check.
5327 3381 if ( ! isset( $spacing_scale['steps'] )
5328 3382 || ! is_numeric( $spacing_scale['steps'] )
5329 3383 || ! isset( $spacing_scale['mediumStep'] )
@@ -5344,101 +3398,8 @@
5344 3398 if ( 0 === $spacing_scale['steps'] ) {
5345 3399 return null;
5346 3400 }
5347 3401
5348 - $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
5349 -
5350 - // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
5351 - if ( $spacing_scale['steps'] <= 7 ) {
5352 - for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
5353 - $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
5354 - }
5355 - }
5356 -
5357 - _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
5358 - }
5359 -
5360 - /**
5361 - * Merges two sets of spacing size presets.
5362 - *
5363 - * @since 6.6.0
5364 - *
5365 - * @param array $base The base set of spacing sizes.
5366 - * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
5367 - * @return array The merged set of spacing sizes.
5368 - */
5369 - private static function merge_spacing_sizes( $base, $incoming ) {
5370 - // Preserve the order if there are no base (spacingScale) values.
5371 - if ( empty( $base ) ) {
5372 - return $incoming;
5373 - }
5374 - $merged = array();
5375 - foreach ( $base as $item ) {
5376 - $merged[ $item['slug'] ] = $item;
5377 - }
5378 - foreach ( $incoming as $item ) {
5379 - $merged[ $item['slug'] ] = $item;
5380 - }
5381 - ksort( $merged, SORT_NUMERIC );
5382 - return array_values( $merged );
5383 - }
5384 -
5385 - /**
5386 - * Generates a set of spacing sizes by starting with a medium size and
5387 - * applying an operator with an increment value to generate the rest of the
5388 - * sizes outward from the medium size. The medium slug is '50' with the rest
5389 - * of the slugs being 10 apart. The generated names use t-shirt sizing.
5390 - *
5391 - * Example:
5392 - *
5393 - * $spacing_scale = array(
5394 - * 'steps' => 4,
5395 - * 'mediumStep' => 16,
5396 - * 'unit' => 'px',
5397 - * 'operator' => '+',
5398 - * 'increment' => 2,
5399 - * );
5400 - * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
5401 - * // -> array(
5402 - * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ),
5403 - * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ),
5404 - * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ),
5405 - * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
5406 - * // )
5407 - *
5408 - * @since 6.6.0
5409 - *
5410 - * @param array $spacing_scale {
5411 - * The spacing scale values. All are required.
5412 - *
5413 - * @type int $steps The number of steps in the scale. (up to 10 steps are supported.)
5414 - * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
5415 - * @type string $unit The CSS unit to use for the sizes.
5416 - * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
5417 - * @type float $increment The value used with the operator to generate the other sizes.
5418 - * }
5419 - * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
5420 - */
5421 - private static function compute_spacing_sizes( $spacing_scale ) {
5422 - /*
5423 - * This condition is intentionally missing some checks on ranges for the values in order to
5424 - * keep backwards compatibility with the previous implementation.
5425 - */
5426 - if (
5427 - ! isset( $spacing_scale['steps'] ) ||
5428 - ! is_numeric( $spacing_scale['steps'] ) ||
5429 - 0 === $spacing_scale['steps'] ||
5430 - ! isset( $spacing_scale['mediumStep'] ) ||
5431 - ! is_numeric( $spacing_scale['mediumStep'] ) ||
5432 - ! isset( $spacing_scale['unit'] ) ||
5433 - ! isset( $spacing_scale['operator'] ) ||
5434 - ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
5435 - ! isset( $spacing_scale['increment'] ) ||
5436 - ! is_numeric( $spacing_scale['increment'] )
5437 - ) {
5438 - return array();
5439 - }
5440 -
5441 3402 $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
5442 3403 $current_step = $spacing_scale['mediumStep'];
5443 3404 $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
5444 3405 $x_small_count = null;
@@ -5519,9 +3480,16 @@
5519 3480 foreach ( $above_sizes as $above_sizes_item ) {
5520 3481 $spacing_sizes[] = $above_sizes_item;
5521 3482 }
5522 3483
5523 - return $spacing_sizes;
3484 + // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
3485 + if ( $spacing_scale['steps'] <= 7 ) {
3486 + for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
3487 + $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
3488 + }
3489 + }
3490 +
3491 + _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
5524 3492 }
5525 3493
5526 3494 /**
5527 3495 * Returns the selectors metadata for a block.
@@ -5595,11 +3563,11 @@
5595 3563
5596 3564 $settings = $this->theme_json['settings'] ?? null;
5597 3565
5598 3566 foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
5599 - // Skip if this is the block's root selector, the custom CSS
5600 - // selector, or the block doesn't have any styles for the feature.
5601 - if ( 'root' === $feature || 'css' === $feature || empty( $node[ $feature ] ) ) {
3567 + // Skip if this is the block's root selector or the block doesn't
3568 + // have any styles for the feature.
3569 + if ( 'root' === $feature || empty( $node[ $feature ] ) ) {
5602 3570 continue;
5603 3571 }
5604 3572
5605 3573 if ( is_array( $feature_selectors ) ) {
@@ -5682,9 +3650,9 @@
5682 3650 $prefix = 'var:';
5683 3651 $prefix_len = strlen( $prefix );
5684 3652 $token_in = '|';
5685 3653 $token_out = '--';
5686 - if ( str_starts_with( $value, $prefix ) ) {
3654 + if ( 0 === strpos( $value, $prefix ) ) {
5687 3655 $unwrapped_name = str_replace(
5688 3656 $token_in,
5689 3657 $token_out,
5690 3658 substr( $value, $prefix_len )
@@ -5706,9 +3674,9 @@
5706 3674 private static function resolve_custom_css_format( $tree ) {
5707 3675 $prefix = 'var:';
5708 3676
5709 3677 foreach ( $tree as $key => $data ) {
5710 - if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
3678 + if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5711 3679 $tree[ $key ] = self::convert_custom_properties( $data );
5712 3680 } elseif ( is_array( $data ) ) {
5713 3681 $tree[ $key ] = self::resolve_custom_css_format( $data );
5714 3682 }
@@ -5720,10 +3688,8 @@
5720 3688 /**
5721 3689 * Replaces CSS variables with their values in place.
5722 3690 *
5723 3691 * @since 6.3.0
5724 - * @since 6.6.0 Check for empty style before processing.
5725 - *
5726 3692 * @param array $styles CSS declarations to convert.
5727 3693 * @param array $values key => value pairs to use for replacement.
5728 3694 * @return array
5729 3695 */
@@ -5728,12 +3694,8 @@
5728 3694 * @return array
5729 3695 */
5730 3696 private static function convert_variables_to_value( $styles, $values ) {
5731 3697 foreach ( $styles as $key => $style ) {
5732 - if ( empty( $style ) ) {
5733 - continue;
5734 - }
5735 -
5736 3698 if ( is_array( $style ) ) {
5737 3699 $styles[ $key ] = self::convert_variables_to_value( $style, $values );
5738 3700 continue;
5739 3701 }
@@ -5754,10 +3716,10 @@
5754 3716 $rule_to_replace,
5755 3717 $fallback,
5756 3718 ),
5757 3719 array(
5758 - $values[ $key_in_values ] ?? $rule_to_replace,
5759 - $values[ $fallback ] ?? $fallback,
3720 + isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace,
3721 + isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback,
5760 3722 ),
5761 3723 $resolved_style
5762 3724 );
5763 3725 }
@@ -5793,129 +3755,6 @@
5793 3755 );
5794 3756
5795 3757 $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
5796 3758 return $theme_json;
5797 - }
5798 -
5799 - /**
5800 - * Generates a selector for a block style variation.
5801 - *
5802 - * @param string $variation_name Name of the block style variation.
5803 - * @param string $block_selector CSS selector for the block.
5804 - *
5805 - * @return string Block selector with block style variation selector added to it.
5806 - */
5807 - protected static function get_block_style_variation_selector( $variation_name, $block_selector ) {
5808 - $variation_class = ".is-style-$variation_name";
5809 -
5810 - if ( ! $block_selector ) {
5811 - return $variation_class;
5812 - }
5813 -
5814 - $limit = 1;
5815 - $selector_parts = static::split_selector_list( $block_selector );
5816 - $result = array();
5817 -
5818 - /*
5819 - * Append the variation class to each selector's ancestor: the first
5820 - * run of characters before any combinator (whitespace) or pseudo-class
5821 - * (`:`). Only the first match is replaced.
5822 - *
5823 - * Examples ("custom" variation):
5824 - * - `.wp-block` => `.wp-block.is-style-custom`
5825 - * - `.wp-block .inner` => `.wp-block.is-style-custom .inner`
5826 - * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)`
5827 - * - `:where(.outer .inner)` => `:where(.outer.is-style-custom .inner)`
5828 - */
5829 - foreach ( $selector_parts as $part ) {
5830 - $result[] = preg_replace_callback(
5831 - '/[^\s:]+/',
5832 - function ( $matches ) use ( $variation_class ) {
5833 - return $matches[0] . $variation_class;
5834 - },
5835 - $part,
5836 - $limit
5837 - );
5838 - }
5839 -
5840 - return implode( ', ', $result );
5841 - }
5842 -
5843 - /**
5844 - * Applies a block style variation class to a feature selector.
5845 - *
5846 - * Feature selectors can target a different element than the block's root
5847 - * selector. For example, the Button block's root selector targets the inner
5848 - * link, while its dimensions width selector targets the outer wrapper. Apply
5849 - * the variation class directly to the selector that will receive the
5850 - * declarations instead of deriving it by subtracting the root selector from
5851 - * the feature selector.
5852 - *
5853 - * @param array $style_variation Style variation metadata.
5854 - * @param string $feature_selector CSS selector for the feature.
5855 - * @return string Feature selector with block style variation selector added.
5856 - */
5857 - protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
5858 - $variation_path = $style_variation['path'] ?? array();
5859 - $variation_name = $style_variation['name'] ?? ( is_array( $variation_path ) ? end( $variation_path ) : null );
5860 -
5861 - if ( ! $variation_name ) {
5862 - return $style_variation['selector'] ?? $feature_selector;
5863 - }
5864 -
5865 - $variation_class = ".is-style-$variation_name";
5866 - $selector_parts = static::split_selector_list( $feature_selector );
5867 - $selector_parts = array_map(
5868 - static function ( $selector ) use ( $variation_class ) {
5869 - $prefix = $variation_class . ' ';
5870 -
5871 - if ( str_starts_with( $selector, $prefix ) ) {
5872 - return substr( $selector, strlen( $prefix ) );
5873 - }
5874 -
5875 - return $selector;
5876 - },
5877 - $selector_parts
5878 - );
5879 -
5880 - return static::get_block_style_variation_selector(
5881 - $variation_name,
5882 - implode( ', ', $selector_parts )
5883 - );
5884 - }
5885 -
5886 - /**
5887 - * Collects valid block style variations keyed by block type.
5888 - *
5889 - * @since 6.6.0
5890 - * @since 6.8.0 Added the `$blocks_metadata` parameter.
5891 - *
5892 - * @param array $blocks_metadata Optional. List of metadata per block. Default is the metadata for all blocks.
5893 - * @return array Valid block style variations by block type.
5894 - */
5895 - protected static function get_valid_block_style_variations( $blocks_metadata = array() ) {
5896 - $valid_variations = array();
5897 - $blocks_metadata = empty( $blocks_metadata ) ? static::get_blocks_metadata() : $blocks_metadata;
5898 - foreach ( $blocks_metadata as $block_name => $block_meta ) {
5899 - if ( ! isset( $block_meta['styleVariations'] ) ) {
5900 - continue;
5901 - }
5902 - $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
5903 - }
5904 -
5905 - return $valid_variations;
5906 - }
5907 -
5908 - /**
5909 - * Extracts the block name from the block metadata path.
5910 - *
5911 - * @since 7.0
5912 - *
5913 - * @param array $block_metadata Block metadata.
5914 - * @return string|null The block name or null if not found.
5915 - */
5916 - private static function get_block_name_from_metadata_path( $block_metadata ) {
5917 - if ( isset( $block_metadata['path'] ) ) {
5918 - return $block_metadata['path'][2];
5919 - }
5920 3759 }
5921 3760 }