PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 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 All 404 releases
← All changes | lib/class-wp-theme-json-gutenberg.php +1156 -179 23.0.1 → trunk View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 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 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -289,8 +299,9 @@
289 299 'margin-right' => array( 'spacing', 'margin', 'right' ),
290 300 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
291 301 'margin-left' => array( 'spacing', 'margin', 'left' ),
292 302 'min-height' => array( 'dimensions', 'minHeight' ),
303 + 'min-width' => array( 'dimensions', 'minWidth' ),
293 304 'outline-color' => array( 'outline', 'color' ),
294 305 'outline-offset' => array( 'outline', 'offset' ),
295 306 'outline-style' => array( 'outline', 'style' ),
296 307 'outline-width' => array( 'outline', 'width' ),
@@ -304,8 +315,9 @@
304 315 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
305 316 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
306 317 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
307 318 'text-decoration' => array( 'typography', 'textDecoration' ),
319 + 'text-shadow' => array( 'typography', 'textShadow' ),
308 320 'text-transform' => array( 'typography', 'textTransform' ),
309 321 'text-indent' => array( 'typography', 'textIndent' ),
310 322 'filter' => array( 'filter', 'duotone' ),
311 323 'box-shadow' => array( 'shadow' ),
@@ -387,10 +399,13 @@
387 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
388 400 * @since 6.4.0 Added `layout.allowEditing`.
389 401 * @since 6.4.0 Added `lightbox`.
390 402 * @since 7.0.0 Added type markers to the schema for boolean values.
391 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
392 404 * `typography.textIndent` properties.
405 + * @since 7.1.0 Added `viewport` property.
406 + * @since 7.2.0 Added `typography.textShadow`, `typography.textShadowPresets`,
407 + * and `typography.defaultTextShadowPresets`.
393 408 * @var array
394 409 */
395 410 const VALID_SETTINGS = array(
396 411 'appearanceTools' => null,
@@ -399,8 +414,11 @@
399 414 'backgroundImage' => null,
400 415 'backgroundSize' => null,
401 416 'gradient' => null,
402 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
403 421 'border' => array(
404 422 'color' => null,
405 423 'radius' => null,
406 424 'style' => null,
@@ -431,8 +449,9 @@
431 449 'defaultAspectRatios' => null,
432 450 'dimensionSizes' => null,
433 451 'height' => null,
434 452 'minHeight' => null,
453 + 'minWidth' => null,
435 454 'width' => null,
436 455 ),
437 456 'layout' => array(
438 457 'contentSize' => null,
@@ -462,25 +481,32 @@
462 481 'presets' => null,
463 482 'defaultPresets' => null,
464 483 ),
465 484 'typography' => array(
466 - 'fluid' => null,
467 - 'customFontSize' => null,
468 - 'defaultFontSizes' => null,
469 - 'dropCap' => null,
470 - 'fontFamilies' => null,
471 - 'fontSizes' => null,
472 - 'fontStyle' => null,
473 - 'fontWeight' => null,
474 - 'letterSpacing' => null,
475 - 'lineHeight' => null,
476 - 'textAlign' => null,
477 - 'textColumns' => null,
478 - 'textDecoration' => null,
479 - 'textIndent' => null,
480 - 'textTransform' => null,
481 - 'writingMode' => null,
485 + 'fluid' => null,
486 + 'customFontSize' => null,
487 + 'defaultFontSizes' => null,
488 + 'dropCap' => null,
489 + 'fontFamilies' => null,
490 + 'fontSizes' => null,
491 + 'fontStyle' => null,
492 + 'fontWeight' => null,
493 + 'letterSpacing' => null,
494 + 'lineHeight' => null,
495 + 'textAlign' => null,
496 + 'textColumns' => null,
497 + 'textDecoration' => null,
498 + 'textIndent' => null,
499 + 'textTransform' => null,
500 + 'textShadow' => null,
501 + 'defaultTextShadowPresets' => null,
502 + 'textShadowPresets' => null,
503 + 'writingMode' => null,
482 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
483 509 );
484 510
485 511 const FONT_FAMILY_SCHEMA = array(
486 512 array(
@@ -519,9 +545,9 @@
519 545 * updated `blockGap` to be allowed at any level.
520 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
521 547 * @since 6.6.0 Added `background` sub properties to top-level only.
522 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
523 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
524 550 * `typography.textIndent` properties.
525 551 * @var array
526 552 */
527 553 const VALID_STYLES = array(
@@ -551,8 +577,9 @@
551 577 'dimensions' => array(
552 578 'aspectRatio' => null,
553 579 'height' => null,
554 580 'minHeight' => null,
581 + 'minWidth' => null,
555 582 'width' => null,
556 583 ),
557 584 'filter' => array(
558 585 'duotone' => null,
@@ -579,8 +606,9 @@
579 606 'textAlign' => null,
580 607 'textColumns' => null,
581 608 'textDecoration' => null,
582 609 'textIndent' => null,
610 + 'textShadow' => null,
583 611 'textTransform' => null,
584 612 'writingMode' => null,
585 613 ),
586 614 'css' => null,
@@ -617,16 +645,187 @@
617 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
618 646 );
619 647
620 648 /**
649 + * Default viewport breakpoint sizes.
650 + *
651 + * @since 7.1.0
652 + * @var array
653 + */
654 + const DEFAULT_VIEWPORT_BREAKPOINTS = array(
655 + 'mobile' => '480px',
656 + 'tablet' => '782px',
657 + );
658 +
659 + /**
660 + * Returns CSS media queries for responsive viewport style states.
661 + *
662 + * Breakpoint values are read from `settings.viewport`, sanitized, and
663 + * normalized before the media query strings are generated. By default, the
664 + * returned keys are the theme.json style-state names (`@mobile`, `@tablet`).
665 + * When `$options['include_desktop']` is truthy, `@desktop` is included.
666 + *
667 + * @since 7.1.0
668 + *
669 + * @param mixed $viewport_settings Viewport settings from theme.json.
670 + * @param array $options {
671 + * Optional. Options for generating media queries.
672 + *
673 + * @type bool $include_desktop Whether to include the desktop media query. Default false.
674 + * }
675 + * @return array Responsive media queries.
676 + */
677 + public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) {
678 + $breakpoints = static::sanitize_viewport_settings( $viewport_settings );
679 +
680 + $responsive_media_queries = array();
681 +
682 + if ( isset( $breakpoints['mobile'] ) ) {
683 + $responsive_media_queries['@mobile'] = "@media (width <= {$breakpoints['mobile']})";
684 + }
685 +
686 + if ( isset( $breakpoints['tablet'] ) ) {
687 + $responsive_media_queries['@tablet'] = isset( $breakpoints['mobile'] )
688 + ? sprintf(
689 + '@media (%s < width <= %s)',
690 + $breakpoints['mobile'],
691 + $breakpoints['tablet']
692 + )
693 + : "@media (width <= {$breakpoints['tablet']})";
694 + }
695 +
696 + if ( ! empty( $options['include_desktop'] ) ) {
697 + if ( isset( $breakpoints['tablet'] ) ) {
698 + $desktop_breakpoint = $breakpoints['tablet'];
699 + } else {
700 + $desktop_breakpoint = $breakpoints['mobile'];
701 + }
702 +
703 + $responsive_media_queries['@desktop'] =
704 + "@media (width > {$desktop_breakpoint})";
705 + }
706 +
707 + return $responsive_media_queries;
708 + }
709 +
710 + /**
711 + * Checks whether a viewport breakpoint value is a safe CSS length.
712 + *
713 + * Viewport breakpoints are limited to numeric `px`, `em`, and `rem` lengths.
714 + * CSS functions, percentages, and other units are rejected because breakpoint
715 + * values are interpolated into generated media queries.
716 + *
717 + * @since 7.1.0
718 + *
719 + * @param mixed $value Value to check.
720 + * @return bool Whether the value is valid.
721 + */
722 + private static function is_valid_viewport_breakpoint_size( $value ) {
723 + if ( ! is_string( $value ) ) {
724 + return false;
725 + }
726 +
727 + $value = trim( $value );
728 + if ( '' === $value ) {
729 + return false;
730 + }
731 +
732 + return 1 === preg_match( '/^(?:\d+|\d*\.\d+)(?:px|em|rem)$/', $value );
733 + }
734 +
735 + /**
736 + * Converts a valid viewport breakpoint size to pixels for ordering checks.
737 + *
738 + * Generated media queries keep the original units. This method only
739 + * normalizes values so `mobile` and `tablet` can be compared safely. `em`
740 + * and `rem` lengths use a 16px base for comparison.
741 + *
742 + * @since 7.1.0
743 + *
744 + * @param mixed $value Viewport breakpoint size.
745 + * @return float|null Viewport breakpoint size in pixels, or null when invalid.
746 + */
747 + private static function get_viewport_breakpoint_value_in_pixels( $value ) {
748 + if ( ! static::is_valid_viewport_breakpoint_size( $value ) ) {
749 + return null;
750 + }
751 +
752 + $value = trim( $value );
753 + $unit = substr( $value, -3 );
754 + if ( 'rem' === $unit ) {
755 + $number = (float) substr( $value, 0, -3 );
756 + } else {
757 + $unit = substr( $value, -2 );
758 + $number = (float) substr( $value, 0, -2 );
759 + }
760 +
761 + /*
762 + * Use the most common browser default font size as the base for em/rem
763 + * media query conversions. This pixel value is only used to compare
764 + * breakpoint order; generated media queries keep the original units.
765 + */
766 + return 'px' === $unit ? $number : $number * 16;
767 + }
768 +
769 + /**
770 + * Sanitizes and normalizes viewport breakpoint settings.
771 + *
772 + * Keeps only supported breakpoint keys, trims valid CSS lengths, and returns
773 + * the default breakpoints when no valid custom breakpoint is provided. When
774 + * only one breakpoint is valid, it remains keyed by its configured state and
775 + * uses a single max-width media query. When `tablet` is not larger than
776 + * `mobile`, it is removed.
777 + *
778 + * @since 7.1.0
779 + *
780 + * @param mixed $viewport_settings Viewport settings from theme.json.
781 + * @return array Sanitized viewport breakpoint settings.
782 + */
783 + private static function sanitize_viewport_settings( $viewport_settings ) {
784 + if ( ! is_array( $viewport_settings ) ) {
785 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
786 + }
787 +
788 + $breakpoints = array();
789 + foreach ( array_keys( static::DEFAULT_VIEWPORT_BREAKPOINTS ) as $breakpoint ) {
790 + $value = $viewport_settings[ $breakpoint ] ?? null;
791 + $px = static::get_viewport_breakpoint_value_in_pixels( $value );
792 + if ( null !== $px ) {
793 + $breakpoints[ $breakpoint ] = array(
794 + 'value' => trim( $value ),
795 + 'px' => $px,
796 + );
797 + }
798 + }
799 +
800 + if ( empty( $breakpoints ) ) {
801 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
802 + }
803 +
804 + if ( 1 === count( $breakpoints ) ) {
805 + $breakpoint = key( $breakpoints );
806 + return array( $breakpoint => $breakpoints[ $breakpoint ]['value'] );
807 + }
808 +
809 + $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] );
810 +
811 + if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px']
812 + ) {
813 + $sanitized['tablet'] = $breakpoints['tablet']['value'];
814 + }
815 +
816 + return $sanitized;
817 + }
818 +
819 + /**
621 820 * Custom states for blocks that map to CSS class selectors rather than
622 - * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
623 - * distinguish them from real CSS pseudo-selectors.
821 + * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
822 + * distinguish them from real CSS pseudo-selectors and breakpoint states.
624 823 *
625 824 * The CSS selector for each state is defined in the block's block.json
626 825 * under `selectors.states`, e.g.:
627 826 *
628 - * "selectors": { "states": { "@current": ".some-css-selector" } }
827 + * "selectors": { "states": { "-current": ".some-css-selector" } }
629 828 *
630 829 * This constant controls which states are valid in theme.json for a given
631 830 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
632 831 * as valid sub-states, producing compound selectors such as
@@ -634,9 +833,9 @@
634 833 *
635 834 * @var array
636 835 */
637 836 const VALID_BLOCK_CUSTOM_STATES = array(
638 - 'core/navigation-link' => array( '@current' ),
837 + 'core/navigation-link' => array( '-current' ),
639 838 );
640 839
641 840 /**
642 841 * The valid elements that can be found under styles.
@@ -658,8 +857,9 @@
658 857 'button' => '.wp-element-button, .wp-block-button__link',
659 858 // The block classes are necessary to target older content that won't use the new class names.
660 859 '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',
661 860 'cite' => 'cite',
861 + 'label' => 'label',
662 862 'select' => 'select',
663 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
664 864 );
665 865
@@ -731,17 +931,35 @@
731 931
732 932 /**
733 933 * Processes pseudo-selectors for any node (block or variation).
734 934 *
735 - * @param array $node The node data (block or variation).
736 - * @param string $base_selector The base selector.
737 - * @param array $settings The theme settings.
738 - * @param string $block_name The block name.
935 + * @since 7.0.0
936 + *
937 + * @param array $node The node data (block or variation).
938 + * @param string $base_selector The base selector.
939 + * @param array $settings The theme settings.
940 + * @param string $block_name The block name.
941 + * @param array|null $block_metadata Metadata about the block to get styles for.
942 + * @param array|null $style_variation Style variation metadata.
739 943 * @return array Array of pseudo-selector declarations.
740 944 */
741 - private static function process_pseudo_selectors( $node, $base_selector, $settings, $block_name ) {
945 + private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
742 946 $pseudo_declarations = array();
947 + $add_declarations = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
948 + if ( empty( $declarations ) ) {
949 + return;
950 + }
743 951
952 + if ( isset( $pseudo_declarations[ $selector ] ) ) {
953 + $pseudo_declarations[ $selector ] = array_merge(
954 + $pseudo_declarations[ $selector ],
955 + $declarations
956 + );
957 + } else {
958 + $pseudo_declarations[ $selector ] = $declarations;
959 + }
960 + };
961 +
744 962 if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
745 963 return $pseudo_declarations;
746 964 }
747 965
@@ -746,11 +964,28 @@
746 964 }
747 965
748 966 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
749 967 if ( isset( $node[ $pseudo_selector ] ) ) {
750 - $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
751 - $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
752 - $pseudo_declarations[ $combined_selector ] = $declarations;
968 + $pseudo_node = $node[ $pseudo_selector ];
969 +
970 + if ( is_array( $block_metadata ) ) {
971 + $feature_declarations = $this->get_feature_declarations_for_node( $block_metadata, $pseudo_node );
972 + $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
973 + $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
974 +
975 + foreach ( $feature_declarations as $feature_selector => $declarations ) {
976 + $target_selector = is_array( $style_variation )
977 + ? static::get_block_style_variation_feature_selector( $style_variation, $feature_selector )
978 + : $feature_selector;
979 + $combined_selector = static::append_to_selector( $target_selector, $pseudo_selector );
980 +
981 + $add_declarations( $combined_selector, $declarations );
982 + }
983 + }
984 +
985 + $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
986 + $declarations = static::compute_style_properties( $pseudo_node, $settings, null, null );
987 + $add_declarations( $combined_selector, $declarations );
753 988 }
754 989 }
755 990
756 991 return $pseudo_declarations;
@@ -796,8 +1031,9 @@
796 1031 array( 'color', 'caption' ),
797 1032 array( 'dimensions', 'aspectRatio' ),
798 1033 array( 'dimensions', 'height' ),
799 1034 array( 'dimensions', 'minHeight' ),
1035 + array( 'dimensions', 'minWidth' ),
800 1036 array( 'dimensions', 'width' ),
801 1037 // BEGIN EXPERIMENTAL.
802 1038 // Allow `position.fixed` to be opted-in by default.
803 1039 // Sticky position support was backported to WordPress 6.2 in https://core.trac.wordpress.org/ticket/57618.
@@ -837,9 +1073,12 @@
837 1073 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
838 1074 $origin = 'theme';
839 1075 }
840 1076
841 - $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
1077 + $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
1078 + if ( isset( $this->theme_json['styles'] ) ) {
1079 + $this->theme_json['styles'] = gutenberg_resolve_style_state_aliases( $this->theme_json['styles'] );
1080 + }
842 1081 $blocks_metadata = static::get_blocks_metadata();
843 1082 $valid_block_names = array_keys( $blocks_metadata );
844 1083 $valid_element_names = array_keys( static::ELEMENTS );
845 1084 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
@@ -1008,8 +1247,10 @@
1008 1247 *
1009 1248 * @since 5.8.0
1010 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1011 1250 * @since 6.6.0 Extended schema definition to allow enhanced block style variations.
1251 + * @since 7.1.1 Updated schema to allow responsive breakpoint states and pseudo-selectors
1252 + * at the top level of `styles` for block style variation partials.
1012 1253 *
1013 1254 * @param array $input Structure to sanitize.
1014 1255 * @param array $valid_block_names List of valid block names.
1015 1256 * @param array $valid_element_names List of valid element names.
@@ -1044,17 +1285,19 @@
1044 1285 }
1045 1286 }
1046 1287
1047 1288 // Build the schema based on valid block & element names.
1048 - $schema = array();
1049 - $schema_styles_elements = array();
1289 + $schema = array();
1290 + $schema_styles_elements = array();
1291 + $responsive_media_queries = static::get_viewport_media_queries( $input['settings']['viewport'] ?? null );
1050 1292
1051 1293 /*
1052 - * Set allowed element pseudo selectors based on per element allow list.
1294 + * Set allowed element pseudo selectors and responsive breakpoint states.
1053 1295 * Target data structure in schema:
1054 1296 * e.g.
1055 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1056 1298 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1299 + * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1057 1300 */
1058 1301 foreach ( $valid_element_names as $element ) {
1059 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1060 1303
@@ -1062,28 +1305,62 @@
1062 1305 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
1063 1306 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
1064 1307 }
1065 1308 }
1309 +
1310 + // Add responsive breakpoint states for elements.
1311 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1312 + $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1313 + }
1066 1314 }
1067 1315
1068 1316 $schema_styles_blocks = array();
1069 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1070 1319
1320 + $common_block_settings = static::VALID_SETTINGS;
1321 + // `viewport` and `blockVisibility` are global-only settings and cannot be set per block for now.
1322 + unset(
1323 + $common_block_settings['viewport'],
1324 + $common_block_settings['blockVisibility']
1325 + );
1326 +
1071 1327 /*
1072 1328 * Generate a schema for blocks.
1073 - * - Block styles can contain `elements` & `variations` definitions.
1329 + * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1074 1330 * - Variations definitions cannot be nested.
1075 - * - Variations can contain styles for inner `blocks`.
1076 - * - Variation inner `blocks` styles can contain `elements`.
1331 + * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states.
1332 + * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1077 1333 *
1078 - * As each variation needs a `blocks` schema but further nested
1079 - * inner `blocks`, the overall schema will be generated in multiple passes.
1334 + * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1335 + * for further nested inner `blocks`, the overall schema is generated in multiple passes.
1336 + *
1337 + * All blocks start with the same style schema. Build that common schema
1338 + * once, then add block-specific pseudo and custom states below.
1080 1339 */
1340 + $responsive_block_schema = $styles_non_top_level;
1341 + $responsive_block_schema['elements'] = $schema_styles_elements;
1342 +
1343 + $common_block_schema = $styles_non_top_level;
1344 + $common_block_schema['elements'] = $schema_styles_elements;
1345 +
1346 + foreach ( $breakpoint_states as $breakpoint_state ) {
1347 + $common_block_schema[ $breakpoint_state ] = $responsive_block_schema;
1348 + }
1349 +
1081 1350 foreach ( $valid_block_names as $block ) {
1082 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1083 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1084 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1085 1353
1354 + // Add responsive pseudo-selectors only to blocks that support them.
1355 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1356 + foreach ( $breakpoint_states as $breakpoint_state ) {
1357 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1358 + $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1359 + }
1360 + }
1361 + }
1362 +
1086 1363 // Add pseudo-selectors for blocks that support them.
1087 1364 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1088 1365 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1089 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1089,9 +1366,9 @@
1089 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1090 1367 }
1091 1368 }
1092 1369
1093 - // Add custom states for blocks that support them (e.g. '@current' for navigation).
1370 + // Add custom states for blocks that support them (e.g. '-current' for navigation).
1094 1371 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1095 1372 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1096 1373 $custom_state_schema = $styles_non_top_level;
1097 1374 // The same pseudo-selectors valid for the block at the top level
@@ -1128,8 +1405,20 @@
1128 1405 if ( ! empty( $style_variation_names ) ) {
1129 1406 foreach ( $style_variation_names as $variation_name ) {
1130 1407 $variation_schema = $block_style_variation_styles;
1131 1408
1409 + // Add responsive breakpoint states to block style variations.
1410 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1411 + $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1412 + $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1413 +
1414 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1415 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1416 + $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1417 + }
1418 + }
1419 + }
1420 +
1132 1421 // Add pseudo-selectors to variations for blocks that support them.
1133 1422 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1134 1423 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1135 1424 $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
@@ -1149,8 +1438,49 @@
1149 1438 $schema['settings'] = static::VALID_SETTINGS;
1150 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1151 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1152 1441
1442 + /*
1443 + * Add block style variation states to the top-level styles schema.
1444 + *
1445 + * Block style variations defined in a standalone JSON partial within a
1446 + * theme's `styles` directory declare their styles at the root of the
1447 + * `styles` object, so they are sanitized against the top-level schema.
1448 + * It needs to allow the same states that are allowed for variations
1449 + * declared inline in theme.json, otherwise those states are silently
1450 + * removed as unknown keys.
1451 + *
1452 + * The `blockTypes` property is only present on block style variation
1453 + * partials, so it both identifies the config as a variation and
1454 + * determines which pseudo-selectors are valid for it. Regular
1455 + * theme.json files are unaffected.
1456 + */
1457 + if ( ! empty( $input['blockTypes'] ) && is_array( $input['blockTypes'] ) ) {
1458 + $variation_pseudo_selectors = array();
1459 + foreach ( $input['blockTypes'] as $variation_block_type ) {
1460 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] ) ) {
1461 + $variation_pseudo_selectors = array_merge(
1462 + $variation_pseudo_selectors,
1463 + static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ]
1464 + );
1465 + }
1466 + }
1467 + $variation_pseudo_selectors = array_unique( $variation_pseudo_selectors );
1468 +
1469 + foreach ( $breakpoint_states as $breakpoint_state ) {
1470 + $schema['styles'][ $breakpoint_state ] = $styles_non_top_level;
1471 + $schema['styles'][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1472 +
1473 + foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1474 + $schema['styles'][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1475 + }
1476 + }
1477 +
1478 + foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1479 + $schema['styles'][ $pseudo_selector ] = $styles_non_top_level;
1480 + }
1481 + }
1482 +
1153 1483 // Remove anything that's not present in the schema.
1154 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1155 1485 if ( ! isset( $input[ $subtree ] ) ) {
1156 1486 continue;
@@ -1162,8 +1492,12 @@
1162 1492 }
1163 1493
1164 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1165 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1166 1500 if ( empty( $result ) ) {
1167 1501 unset( $output[ $subtree ] );
1168 1502 } else {
1169 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1191,14 +1525,38 @@
1191 1525 protected static function append_to_selector( $selector, $to_append ) {
1192 1526 if ( ! str_contains( $selector, ',' ) ) {
1193 1527 return $selector . $to_append;
1194 1528 }
1529 +
1530 + /**
1531 + * Check for an opportunity to skip the more-costly selector splitting.
1532 + * This should be possible if there are no comments, strings, functions,
1533 + * URLs, escapes, or comment declaration openers (CDOs).
1534 + *
1535 + * Note that this means the fast-path will not apply for selectors like
1536 + * the following incomplete list:
1537 + *
1538 + * - `[class ~= "wide"]`
1539 + * - `.wp-block:is(.is-style-a, .is-style-b)`
1540 + * - `:nth-child(1)`
1541 + *
1542 + * These syntax forms all present opportunities where a comma may not
1543 + * separate selectors. If none of the start characters are present,
1544 + * there should be no way for a comma to mean anything other than a
1545 + * comma token. The exception are syntax errors, which are not handled here.
1546 + *
1547 + * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1548 + */
1549 + if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1550 + return str_replace( ',', $to_append . ',', $selector ) . $to_append;
1551 + }
1552 +
1195 1553 $new_selectors = array();
1196 - $selectors = explode( ',', $selector );
1554 + $selectors = static::split_selector_list( $selector );
1197 1555 foreach ( $selectors as $sel ) {
1198 1556 $new_selectors[] = $sel . $to_append;
1199 1557 }
1200 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1201 1559 }
1202 1560
1203 1561 /**
1204 1562 * Prepends a sub-selector to an existing one.
@@ -1216,17 +1574,199 @@
1216 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1217 1575 if ( ! str_contains( $selector, ',' ) ) {
1218 1576 return $to_prepend . $selector;
1219 1577 }
1578 +
1579 + /**
1580 + * Check for an opportunity to skip the more-costly selector splitting.
1581 + * This should be possible if there are no comments, strings, functions,
1582 + * URLs, escapes, or comment declaration openers (CDOs).
1583 + *
1584 + * Note that this means the fast-path will not apply for selectors like
1585 + * the following incomplete list:
1586 + *
1587 + * - `[class ~= "wide"]`
1588 + * - `.wp-block:is(.is-style-a, .is-style-b)`
1589 + * - `:nth-child(1)`
1590 + *
1591 + * These syntax forms all present opportunities where a comma may not
1592 + * separate selectors. If none of the start characters are present,
1593 + * there should be no way for a comma to mean anything other than a
1594 + * comma token. The exception are syntax errors, which are not handled here.
1595 + *
1596 + * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1597 + */
1598 + if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1599 + return $to_prepend . str_replace( ',', ',' . $to_prepend, $selector );
1600 + }
1601 +
1220 1602 $new_selectors = array();
1221 - $selectors = explode( ',', $selector );
1603 + $selectors = static::split_selector_list( $selector );
1222 1604 foreach ( $selectors as $sel ) {
1223 1605 $new_selectors[] = $to_prepend . $sel;
1224 1606 }
1225 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1226 1609 }
1227 1610
1228 1611 /**
1612 + * Splits a selector list into separate selectors.
1613 + *
1614 + * While selectors are joined by commas, not all commas separate top-level selectors.
1615 + * This method only separates top-level selectors, so some commas may appear inside
1616 + * strings, nested selectors, and comments. Leading and trailing CSS whitespace is
1617 + * trimmed from the returned list items.
1618 + *
1619 + * Non-selector content, such as comments, are retained in the list in the same item
1620 + * as the selector content they follow.
1621 + *
1622 + * Example:
1623 + *
1624 + * array( '.wp-block' ) === self::split_selector_list( '.wp-block' );
1625 + * array( '.one', '.two' ) === self::split_selector_list( '.one, .two' );
1626 + *
1627 + * // Nested selector lists are retained within their containing selector.
1628 + * array( ':is(.a, .b)', 'c' ) === self::split_selector_list( ':is(.a, .b), .c' );
1629 + *
1630 + * // Commas within strings do not separate selectors.
1631 + * $selectors = self::split_selector_list( '[data-label="Save, continue"],.fallback' );
1632 + * $selectors === array( '[data-label="Save, continue"]', '.fallback' )
1633 + *
1634 + * array( 'lang(zh, "*-hant")', '.foo' ) === self::split_selector_list( 'lang(zh, "*-hant"), .foo' );
1635 + *
1636 + * // Identifiers may contain escaped commas.
1637 + * array( '.foo\,bar', '.baz' ) === self::split_selector_list( '.foo\,bar,.baz' );
1638 + *
1639 + * // Comments stay with the selector they follow.
1640 + * array( '.a /* a, the first *\/', '.b' ) === self::split_selector_list( '.a /* a, the first *\/,.b' );
1641 + *
1642 + * @link https://www.w3.org/TR/selectors/#parse-selector
1643 + * @link https://www.w3.org/TR/css-syntax-3/
1644 + *
1645 + * @param string $selector CSS selector list.
1646 + * @return string[] Selectors.
1647 + */
1648 + protected static function split_selector_list( $selector ): array {
1649 + if ( ! str_contains( $selector, ',' ) ) {
1650 + // See note on trimming CSS whitespace in main loop.
1651 + return array( trim( $selector, " \t\n" ) );
1652 + }
1653 +
1654 + $selectors = array();
1655 + $selector_length = strlen( $selector );
1656 + $parentheses_depth = 0;
1657 + $at = 0;
1658 + $was_at = 0;
1659 +
1660 + while ( $at < $selector_length ) {
1661 + $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1662 + if ( $next_at >= $selector_length ) {
1663 + break;
1664 + }
1665 +
1666 + $next_cp = $selector[ $next_at ];
1667 +
1668 + // Escaped syntax characters do not act as delimiters.
1669 + if ( '\\' === $next_cp ) {
1670 + $at = min( $next_at + 2, $selector_length );
1671 + continue;
1672 + }
1673 +
1674 + /*
1675 + * Start of a parenthesized expression, which maintains a stack of parentheses.
1676 + * For the sake of this function, no selector list will be split inside parentheses.
1677 + * Therefore it’s possible to jump ahead until this list completes.
1678 + */
1679 + if ( '(' === $next_cp || ')' === $next_cp ) {
1680 + $parentheses_depth += '(' === $next_cp ? 1 : -1;
1681 + $at = $next_at + 1;
1682 + continue;
1683 + }
1684 +
1685 + // Start of a string, which will be incorporated into the selector in which it’s found.
1686 + if ( "'" === $next_cp || '"' === $next_cp ) {
1687 + $end_of_string = $next_at + 1;
1688 + while ( $end_of_string < $selector_length ) {
1689 + $end_of_string += strcspn( $selector, "{$next_cp}\\", $end_of_string );
1690 + if ( $end_of_string >= $selector_length ) {
1691 + break;
1692 + }
1693 +
1694 + $end_cp = $selector[ $end_of_string ];
1695 +
1696 + // Skip escaped characters.
1697 + if ( '\\' === $end_cp ) {
1698 + $end_of_string = $end_of_string + 2;
1699 + continue;
1700 + }
1701 +
1702 + if ( $next_cp === $end_cp ) {
1703 + ++$end_of_string;
1704 + break;
1705 + }
1706 +
1707 + ++$end_of_string;
1708 + }
1709 +
1710 + $at = $end_of_string;
1711 + continue;
1712 + }
1713 +
1714 + // Start of a comment, which will be incorporated into the selector in which it’s found.
1715 + if ( '/' === $next_cp && ( $next_at + 1 ) < $selector_length && '*' === $selector[ $next_at + 1 ] ) {
1716 + $comment_end_at = strpos( $selector, '*/', $next_at + 1 );
1717 + $is_terminated = false !== $comment_end_at;
1718 + $after_comment = $is_terminated ? $comment_end_at + 2 : strlen( $selector );
1719 + $at = $after_comment;
1720 + continue;
1721 + }
1722 +
1723 + // Start of a CDO or CDC, which will be incorporated into the selector in which it’s found.
1724 + if (
1725 + ( '<' === $next_cp && 0 === substr_compare( $selector, '<!--', $next_at, 4 ) ) ||
1726 + ( '-' === $next_cp && 0 === substr_compare( $selector, '-->', $next_at, 3 ) )
1727 + ) {
1728 + $at = $next_at + ( '<' === $next_cp ? 4 : 3 );
1729 + continue;
1730 + }
1731 +
1732 + // Everything else is either a comma token or part of a selector.
1733 + if ( ',' === $next_cp && 0 === $parentheses_depth ) {
1734 + /**
1735 + * Trim each selector so that downstream code doesn’t see whitespace
1736 + * as the first character in a selector and get confused.
1737 + *
1738 + * There is inconsistency in this because comments and other syntax
1739 + * are included which are also not part of the selector itself, but
1740 + * a tradeoff is made between removing common syntax which carries
1741 + * no meaning and rarer syntax which leaves auxiliary information.
1742 + *
1743 + * > A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
1744 + * > Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are
1745 + * > not included in this definition, as they are converted
1746 + * > to U+000A LINE FEED during preprocessing.
1747 + *
1748 + * @link https://www.w3.org/TR/css-syntax/#whitespace
1749 + * @link https://www.w3.org/TR/css-syntax/#newline
1750 + */
1751 + $selectors[] = trim( substr( $selector, $was_at, $next_at - $was_at ), " \t\n" );
1752 + $at = $next_at + 1;
1753 + $was_at = $at;
1754 + continue;
1755 + }
1756 +
1757 + $at = $next_at + 1;
1758 + }
1759 +
1760 + if ( $was_at < $selector_length ) {
1761 + // See note on trimming CSS whitespace in main loop.
1762 + $selectors[] = trim( substr( $selector, $was_at ), " \t\n" );
1763 + }
1764 +
1765 + return $selectors;
1766 + }
1767 +
1768 + /**
1229 1769 * Returns the metadata for each block.
1230 1770 *
1231 1771 * Example:
1232 1772 *
@@ -1643,9 +2183,9 @@
1643 2183 /**
1644 2184 * Returns the global styles custom CSS for a single block.
1645 2185 * This function is deprecated; please do not sync to core.
1646 2186 *
1647 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1648 2188 * @param string $selector The block selector.
1649 2189 *
1650 2190 * @return string The global styles custom CSS for the block.
1651 2191 */
@@ -1770,9 +2310,10 @@
1770 2310
1771 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1772 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1773 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1774 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1775 2316 // Use a fallback gap value if block gap support is not available.
1776 2317 if ( ! $has_block_gap_support ) {
1777 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1778 2319 if ( ! empty( $block_type ) ) {
@@ -1780,18 +2321,29 @@
1780 2321 }
1781 2322 } else {
1782 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1783 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1784 2326
1785 2327 // Support split row / column values and concatenate to a shorthand value.
1786 2328 if ( is_array( $block_gap_value ) ) {
1787 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1788 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1789 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1790 - $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
2329 + $has_block_gap_row_value = isset( $block_gap_value['top'] );
2330 + $has_block_gap_column_value = isset( $block_gap_value['left'] );
2331 +
2332 + if ( $has_block_gap_row_value || $has_block_gap_column_value ) {
2333 + $block_gap_row_value = $has_block_gap_row_value
2334 + ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) )
2335 + : '0';
2336 + $block_gap_column_value = $has_block_gap_column_value
2337 + ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) )
2338 + : '0';
2339 + $block_gap_value = $block_gap_row_value === $block_gap_column_value
2340 + ? $block_gap_row_value
2341 + : $block_gap_row_value . ' ' . $block_gap_column_value;
1791 2342 } else {
1792 - // Skip outputting gap value if not all sides are provided.
1793 - $block_gap_value = null;
2343 + // Skip outputting a gap value if neither supported axis is provided.
2344 + $block_gap_value = null;
2345 + $block_gap_row_value = null;
1794 2346 }
1795 2347 }
1796 2348
1797 2349 // If the block should have custom gap, add the gap styles.
@@ -1801,10 +2353,13 @@
1801 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1802 2354 continue;
1803 2355 }
1804 2356
1805 - $class_name = $layout_definition['className'] ?? false;
1806 - $spacing_rules = $layout_definition['spacingStyles'] ?? array();
2357 + $class_name = $layout_definition['className'] ?? false;
2358 + $spacing_rules = $layout_definition['spacingStyles'] ?? array();
2359 + $layout_gap_value = in_array( $layout_definition_key, array( 'default', 'constrained' ), true )
2360 + ? $block_gap_row_value
2361 + : $block_gap_value;
1807 2362
1808 2363 if (
1809 2364 ! empty( $class_name ) &&
1810 2365 ! empty( $spacing_rules )
@@ -1817,9 +2372,9 @@
1817 2372 ! empty( $spacing_rule['rules'] )
1818 2373 ) {
1819 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1820 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1821 - $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
2376 + $current_css_value = is_string( $css_value ) ? $css_value : $layout_gap_value;
1822 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1823 2378 $declarations[] = array(
1824 2379 'name' => $css_property,
1825 2380 'value' => $current_css_value,
@@ -1931,8 +2486,13 @@
1931 2486 }
1932 2487 }
1933 2488 }
1934 2489 }
2490 +
2491 + if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) {
2492 + $block_rules = $options['media_query'] . '{' . $block_rules . '}';
2493 + }
2494 +
1935 2495 return $block_rules;
1936 2496 }
1937 2497
1938 2498 /**
@@ -1953,9 +2513,9 @@
1953 2513 * .has-value-gradient-background {
1954 2514 * background: value;
1955 2515 * }
1956 2516 *
1957 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
1958 2518 * background: value;
1959 2519 * }
1960 2520 *
1961 2521 * @since 5.9.0
@@ -2156,10 +2716,18 @@
2156 2716 foreach ( $slugs as $slug ) {
2157 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2158 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2159 2719
2160 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2161 - $new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
2720 + /*
2721 + * $selector is often empty (root-level presets), in which case the
2722 + * bare class is used. For block-level presets the block selector is
2723 + * wrapped in `:where()` so the class keeps the same 0-1-0 specificity
2724 + * as a root-level preset. Without this, block-level palette rules
2725 + * (e.g. `p.has-x-color`) out-rank equally-important rules that also
2726 + * target the same property at 0-1-0, such as per-instance responsive
2727 + * state styles.
2728 + */
2729 + $new_selector = '' === $selector ? $class_name : ':where(' . $selector . ')' . $class_name;
2162 2730 $stylesheet .= static::to_ruleset(
2163 2731 $new_selector,
2164 2732 array(
2165 2733 array(
@@ -2196,16 +2764,14 @@
2196 2764 if ( ! $scope || ! $selector ) {
2197 2765 return $selector;
2198 2766 }
2199 2767
2200 - $scopes = explode( ',', $scope );
2201 - $selectors = explode( ',', $selector );
2768 + $scopes = static::split_selector_list( $scope );
2769 + $selectors = static::split_selector_list( $selector );
2202 2770
2203 2771 $selectors_scoped = array();
2204 2772 foreach ( $scopes as $outer ) {
2205 2773 foreach ( $selectors as $inner ) {
2206 - $outer = trim( $outer );
2207 - $inner = trim( $inner );
2208 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2209 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2210 2776 } elseif ( empty( $outer ) ) {
2211 2777 $selectors_scoped[] = $inner;
@@ -2511,15 +3077,15 @@
2511 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2512 3078 * @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.
2513 3079 * @since 6.7.0 Allow ref resolution of background properties.
2514 3080 *
2515 - * @param array $styles Styles to process.
2516 - * @param array $settings Theme settings.
2517 - * @param array $properties Properties metadata.
2518 - * @param array $theme_json Theme JSON array.
2519 - * @param string $selector The style block selector.
3081 + * @param array $styles Styles to process.
3082 + * @param array $settings Theme settings.
3083 + * @param array $properties Properties metadata.
3084 + * @param array $theme_json Theme JSON array.
3085 + * @param string $selector The style block selector.
2520 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2521 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2522 3088 */
2523 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2524 3090 if ( empty( $styles ) ) {
2525 3091 return array();
@@ -2643,10 +3209,10 @@
2643 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2644 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2645 3211 * @since 6.7.0 Added support for background image refs
2646 3212 *
2647 - * @param array $styles Styles subtree.
2648 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2649 3215 * @param array $theme_json Theme JSON array.
2650 3216 * @return string|array Style property value.
2651 3217 */
2652 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3018,9 +3584,9 @@
3018 3584 * @since 6.1.0
3019 3585 *
3020 3586 * @param array $theme_json The theme.json converted to an array.
3021 3587 * @param array $selectors Optional list of selectors per block.
3022 - * @param array $options {
3588 + * @param array $options {
3023 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3024 3590 *
3025 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3026 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3033,10 +3599,11 @@
3033 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3034 3600 return $nodes;
3035 3601 }
3036 3602
3037 - $include_variations = $options['include_block_style_variations'] ?? false;
3038 - $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3603 + $include_variations = $options['include_block_style_variations'] ?? false;
3604 + $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3605 + $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
3039 3606
3040 3607 // If only node paths are to be returned, skip selector assignment.
3041 3608 if ( ! $include_node_paths_only ) {
3042 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3080,8 +3647,9 @@
3080 3647
3081 3648 if ( $include_variations && isset( $node['variations'] ) ) {
3082 3649 foreach ( $node['variations'] as $variation => $node ) {
3083 3650 $variation_selectors[] = array(
3651 + 'name' => $variation,
3084 3652 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3085 3653 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3086 3654 );
3087 3655 }
@@ -3091,49 +3659,100 @@
3091 3659 'name' => $name,
3092 3660 'path' => $node_path,
3093 3661 'selector' => $selector,
3094 3662 'selectors' => $feature_selectors,
3663 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3095 3664 'duotone' => $duotone_selector,
3096 3665 'variations' => $variation_selectors,
3097 3666 'css' => $selector,
3098 3667 );
3099 3668
3669 + // Responsive block nodes: emit one node per breakpoint that has styles.
3670 + // These are rendered immediately after the base block node so that
3671 + // the cascade order is: .block{} → @media{.block{}}
3672 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3673 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3674 + $nodes[] = array(
3675 + 'name' => $name,
3676 + 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3677 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3678 + 'selector' => $selector,
3679 + 'selectors' => $feature_selectors,
3680 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3681 + 'variations' => $variation_selectors,
3682 + 'css' => $selector,
3683 + );
3684 + }
3685 + }
3686 +
3100 3687 // Handle any pseudo selectors for the block.
3101 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3102 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3103 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) {
3104 - /*
3105 - * Append the pseudo-selector to each feature selector so that
3106 - * get_feature_declarations_for_node generates CSS scoped to the
3107 - * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3108 - * default state (e.g. '.wp-block-button').
3109 - */
3110 - $pseudo_feature_selectors = array();
3111 - foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3112 - if ( is_array( $feature_selector ) ) {
3113 - $pseudo_feature_selectors[ $feature ] = array();
3114 - foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3115 - $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3116 - }
3117 - } else {
3118 - $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3690 + $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3691 + $has_responsive_pseudo = false;
3692 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3693 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3694 + $has_responsive_pseudo = true;
3695 + break;
3696 + }
3697 + }
3698 +
3699 + if ( ! $has_pseudo && ! $has_responsive_pseudo ) {
3700 + continue;
3701 + }
3702 +
3703 + /*
3704 + * Append the pseudo-selector to each feature selector so that
3705 + * get_feature_declarations_for_node generates CSS scoped to the
3706 + * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3707 + * default state (e.g. '.wp-block-button').
3708 + */
3709 + $pseudo_feature_selectors = array();
3710 + foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3711 + if ( is_array( $feature_selector ) ) {
3712 + $pseudo_feature_selectors[ $feature ] = array();
3713 + foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3714 + $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3119 3715 }
3716 + } else {
3717 + $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3120 3718 }
3719 + }
3121 3720
3721 + if ( $has_pseudo ) {
3122 3722 $nodes[] = array(
3123 3723 'name' => $name,
3124 3724 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ),
3125 3725 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3126 3726 'selectors' => $pseudo_feature_selectors,
3727 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3127 3728 'duotone' => $duotone_selector,
3128 3729 'variations' => $variation_selectors,
3129 3730 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3130 3731 );
3131 3732 }
3733 +
3734 + // Responsive pseudo nodes: emit one node per breakpoint that has
3735 + // this pseudo state, immediately after the default pseudo node.
3736 + // Cascade order: .block:hover{} → @media{.block:hover{}}
3737 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3738 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3739 + $nodes[] = array(
3740 + 'name' => $name,
3741 + 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3742 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3743 + 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3744 + 'selectors' => $pseudo_feature_selectors,
3745 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3746 + 'variations' => $variation_selectors,
3747 + 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3748 + );
3749 + }
3750 + }
3132 3751 }
3133 3752 }
3134 3753
3135 - // Handle custom states (e.g. '@current' for navigation).
3754 + // Handle custom states (e.g. '-current' for navigation).
3136 3755 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3137 3756 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3138 3757 if (
3139 3758 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
@@ -3144,8 +3763,9 @@
3144 3763 'name' => $name,
3145 3764 'path' => array( 'styles', 'blocks', $name, $custom_state ),
3146 3765 'selector' => $custom_css_selector,
3147 3766 'selectors' => $feature_selectors,
3767 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3148 3768 'duotone' => $duotone_selector,
3149 3769 'variations' => $variation_selectors,
3150 3770 'css' => $custom_css_selector,
3151 3771 );
@@ -3159,8 +3779,9 @@
3159 3779 'name' => $name,
3160 3780 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
3161 3781 'selector' => $compound_css_selector,
3162 3782 'selectors' => $feature_selectors,
3783 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3163 3784 'duotone' => $duotone_selector,
3164 3785 'variations' => $variation_selectors,
3165 3786 'css' => $compound_css_selector,
3166 3787 );
@@ -3170,39 +3791,84 @@
3170 3791 }
3171 3792 }
3172 3793 }
3173 3794 }
3174 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3175 - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3176 - $node_path = array( 'styles', 'blocks', $name, 'elements', $element );
3795 + /*
3796 + * Elements can be styled outside any breakpoint, inside one, or both,
3797 + * so collect the names from all of those places before looping. An
3798 + * element styled only inside a breakpoint still needs a node.
3799 + */
3800 + $block_node = $theme_json['styles']['blocks'][ $name ] ?? array();
3801 + $element_names = array_keys( $block_node['elements'] ?? array() );
3802 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3803 + $element_names = array_merge(
3804 + $element_names,
3805 + array_keys( $block_node[ $breakpoint ]['elements'] ?? array() )
3806 + );
3807 + }
3808 + $element_names = array_unique( $element_names );
3809 +
3810 + if ( ! empty( $element_names ) ) {
3811 + foreach ( $element_names as $element ) {
3812 + $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3177 3813 if ( $include_node_paths_only ) {
3814 + if ( isset( $block_node['elements'][ $element ] ) ) {
3815 + $nodes[] = array(
3816 + 'path' => $element_path,
3817 + );
3818 + }
3819 + continue;
3820 + }
3821 +
3822 + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3823 + continue;
3824 + }
3825 +
3826 + $element_selector = $selectors[ $name ]['elements'][ $element ];
3827 +
3828 + if ( isset( $block_node['elements'][ $element ] ) ) {
3178 3829 $nodes[] = array(
3179 - 'path' => $node_path,
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3180 3832 );
3181 - continue;
3182 3833 }
3183 3834
3184 - $nodes[] = array(
3185 - 'path' => $node_path,
3186 - 'selector' => $selectors[ $name ]['elements'][ $element ],
3187 - );
3835 + // Responsive element nodes: one node per breakpoint that has
3836 + // styles for this element. Cascade: a{} → @media{a{}}
3837 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3838 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3839 + $nodes[] = array(
3840 + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3841 + 'selector' => $element_selector,
3842 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3843 + );
3844 + }
3845 + }
3188 3846
3189 3847 // Handle any pseudo selectors for the element.
3190 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3191 3849 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3850 + // Emit the default pseudo node only when the default state styles
3851 + // the pseudo. Otherwise get_styles_for_block() falls back to the
3852 + // element's base styles, outputting a rule the theme never defined.
3192 3853 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3193 - $node_path = array( 'styles', 'blocks', $name, 'elements', $element );
3194 - if ( $include_node_paths_only ) {
3854 + $nodes[] = array(
3855 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
3856 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3857 + );
3858 + }
3859 +
3860 + // Responsive element pseudo nodes: one node per breakpoint
3861 + // that has this pseudo state for this element.
3862 + // Cascade: a:hover{} → @media{a:hover{}}
3863 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3864 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3195 3865 $nodes[] = array(
3196 - 'path' => $node_path,
3866 + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3867 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3868 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3197 3869 );
3198 - continue;
3199 3870 }
3200 -
3201 - $nodes[] = array(
3202 - 'path' => $node_path,
3203 - 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
3204 - );
3205 3871 }
3206 3872 }
3207 3873 }
3208 3874 }
@@ -3222,13 +3888,15 @@
3222 3888 *
3223 3889 * @return string Styles for the block.
3224 3890 */
3225 3891 public function get_styles_for_block( $block_metadata ) {
3226 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3227 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3228 - $selector = $block_metadata['selector'];
3229 - $settings = $this->theme_json['settings'] ?? null;
3230 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3892 + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3893 + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3894 + $selector = $block_metadata['selector'];
3895 + $settings = $this->theme_json['settings'] ?? null;
3896 + $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3897 + $media_query = $block_metadata['media_query'] ?? null;
3898 + $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null );
3231 3899
3232 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3233 3901
3234 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3233,20 +3901,24 @@
3233 3901
3234 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
3235 3903 $block_name = $block_metadata['name'] ?? null;
3236 3904 $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
3905 + $block_elements = $block_metadata['elements'] ?? array();
3237 3906
3238 3907 // Update button width declarations for percentage values to use calc() with block gap.
3239 3908 $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
3240 3909
3241 3910 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3242 - $style_variation_declarations = array();
3243 - $style_variation_custom_css = array();
3244 - $style_variation_layout_metadata = array();
3245 - if ( ! empty( $block_metadata['variations'] ) ) {
3911 + // Responsive nodes (those with a media_query) do not process variations — variation responsive
3912 + // CSS is handled by the variation's own responsive nodes or the existing variation loop.
3913 + $style_variation_declarations = array();
3914 + $style_variation_custom_css = array();
3915 + $style_variation_responsive_css = array();
3916 + $style_variation_responsive_pseudo_css = array();
3917 + $style_variation_layout_metadata = array();
3918 + if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) {
3246 3919 foreach ( $block_metadata['variations'] as $style_variation ) {
3247 - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3248 - $clean_style_variation_selector = trim( $style_variation['selector'] );
3920 + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3249 3921
3250 3922 // Generate any feature/subfeature style declarations for the current style variation.
3251 3923 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3252 3924
@@ -3257,27 +3929,10 @@
3257 3929 $variation_declarations = static::update_button_width_declarations( $variation_declarations, $settings );
3258 3930
3259 3931 // Combine selectors with style variation's selector and add to overall style variation declarations.
3260 3932 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3261 - /*
3262 - * Clean up any whitespace between comma separated selectors.
3263 - * This prevents these spaces breaking compound selectors such as:
3264 - * - `.wp-block-list:not(.wp-block-list .wp-block-list)`
3265 - * - `.wp-block-image img, .wp-block-image.my-class img`
3266 - */
3267 - $clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
3268 - $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector );
3933 + $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $current_selector );
3269 3934
3270 - // Prepend the variation selector to the current selector.
3271 - $split_selectors = explode( ',', $shortened_selector );
3272 - $updated_selectors = array_map(
3273 - static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3274 - return $clean_style_variation_selector . $split_selector;
3275 - },
3276 - $split_selectors
3277 - );
3278 - $combined_selectors = implode( ',', $updated_selectors );
3279 -
3280 3935 // Add the new declarations to the overall results under the modified selector.
3281 3936 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3282 3937 }
3283 3938 // Compute declarations for remaining styles not covered by feature level selectors.
@@ -3284,9 +3939,9 @@
3284 3939 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3285 3940
3286 3941 // Process pseudo-selectors for this variation (e.g., :hover, :focus).
3287 3942 $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 );
3288 - $variation_pseudo_declarations = static::process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name );
3943 + $variation_pseudo_declarations = $this->process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3289 3944 $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3290 3945
3291 3946 // Store custom CSS for the style variation.
3292 3947 if ( isset( $style_variation_node['css'] ) ) {
@@ -3296,18 +3951,135 @@
3296 3951 // Store variation metadata and node for layout styles generation.
3297 3952 // Only store if the variation has blockGap defined.
3298 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3299 3954 // Append block selector to the variation selector for proper targeting.
3300 - $variation_metadata_with_selector = $style_variation;
3301 - $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3955 + $variation_metadata_with_selector = $style_variation;
3956 + $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3957 +
3958 + /*
3959 + * `get_layout_styles()` reads `name` as a block name, to check that the block
3960 + * supports layout at all. A variation node's `name` is the variation slug,
3961 + * which is never a registered block, so the check fails and every variation
3962 + * gap rule is discarded. Pass the block the variation belongs to, so the
3963 + * support check answers the question it is actually asking.
3964 + */
3965 + $variation_metadata_with_selector['name'] = $block_name;
3966 +
3302 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3303 3968 'metadata' => $variation_metadata_with_selector,
3304 3969 'node' => $style_variation_node,
3305 3970 );
3306 3971 }
3972 +
3973 + // Store responsive breakpoint CSS for the style variation.
3974 + // This includes both base properties and feature-level selectors.
3975 + $variation_responsive_css = '';
3976 + $variation_responsive_pseudo_css = '';
3977 +
3978 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3979 + if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3980 + continue;
3981 + }
3982 +
3983 + $breakpoint_node = $style_variation_node[ $breakpoint ];
3984 + $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3985 + // Process feature-level declarations for this breakpoint.
3986 + $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3987 + $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3988 + $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
3989 + foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) {
3990 + $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $feature_selector );
3991 +
3992 + $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl );
3993 + $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}';
3994 + }
3995 +
3996 + // Process base properties for this breakpoint.
3997 + $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
3998 + if ( ! empty( $breakpoint_declarations ) ) {
3999 + $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
4000 + $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
4001 + }
4002 +
4003 + $breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
4004 + foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) {
4005 + if ( empty( $pseudo_declarations ) ) {
4006 + continue;
4007 + }
4008 + $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations );
4009 + $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}';
4010 + }
4011 +
4012 + // Process custom CSS for this breakpoint.
4013 + if ( isset( $breakpoint_node['css'] ) ) {
4014 + $breakpoint_custom_css = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] );
4015 + $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}';
4016 + }
4017 +
4018 + // Process blockGap responsive layout styles for this variation.
4019 + if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
4020 + $variation_layout_metadata = $style_variation;
4021 + $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
4022 +
4023 + // The variation slug is not a block name here either. See above.
4024 + $variation_layout_metadata['name'] = $block_name;
4025 +
4026 + $variation_responsive_css .= $this->get_layout_styles(
4027 + $variation_layout_metadata,
4028 + array(
4029 + 'node' => $breakpoint_node,
4030 + 'media_query' => $breakpoint_media,
4031 + )
4032 + );
4033 + }
4034 +
4035 + // Process nested element styles for this breakpoint state.
4036 + if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) {
4037 + foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) {
4038 + if ( ! isset( $block_elements[ $element_name ] ) ) {
4039 + continue;
4040 + }
4041 +
4042 + $variation_element_selector = static::get_block_style_variation_feature_selector( $style_variation, $block_elements[ $element_name ] );
4043 +
4044 + $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json );
4045 + if ( ! empty( $element_declarations ) ) {
4046 + $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations );
4047 + $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}';
4048 + }
4049 +
4050 + if ( isset( $element_node['css'] ) ) {
4051 + $element_custom_css = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector );
4052 + $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}';
4053 + }
4054 +
4055 + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4056 + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4057 + if ( ! isset( $element_node[ $pseudo_selector ] ) ) {
4058 + continue;
4059 + }
4060 +
4061 + $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json );
4062 + if ( empty( $pseudo_declarations ) ) {
4063 + continue;
4064 + }
4065 +
4066 + $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
4067 + $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
4068 + }
4069 + }
4070 + }
4071 + }
4072 + }
4073 +
4074 + if ( ! empty( $variation_responsive_css ) ) {
4075 + $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css;
4076 + }
4077 + if ( ! empty( $variation_responsive_pseudo_css ) ) {
4078 + $style_variation_responsive_pseudo_css[ $style_variation['selector'] ] = $variation_responsive_pseudo_css;
4079 + }
3307 4080 }
3308 4081 }
3309 -
3310 4082 /*
3311 4083 * Get a reference to element name from path.
3312 4084 * $block_metadata['path'] = array( 'styles','elements','link' );
3313 4085 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
@@ -3469,20 +4241,39 @@
3469 4241 }
3470 4242 if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
3471 4243 $block_rules .= $style_variation_custom_css[ $style_variation_selector ];
3472 4244 }
4245 + if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) {
4246 + $block_rules .= $style_variation_responsive_css[ $style_variation_selector ];
4247 + }
3473 4248 }
4249 + /*
4250 + * Responsive pseudo styles must be output after default pseudo styles
4251 + * so viewport state styles win in the cascade.
4252 + */
4253 + foreach ( $style_variation_responsive_pseudo_css as $responsive_pseudo_css ) {
4254 + $block_rules .= $responsive_pseudo_css;
4255 + }
3474 4256
4257 + // Compute selector for block custom CSS.
4258 + $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
4259 + if ( is_array( $css_feature_selector ) ) {
4260 + $css_feature_selector = $css_feature_selector['root'] ?? null;
4261 + }
4262 + $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
4263 +
3475 4264 // 7. Generate and append any custom CSS rules.
3476 4265 if ( isset( $node['css'] ) && ! $is_root_selector ) {
3477 - $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
3478 - if ( is_array( $css_feature_selector ) ) {
3479 - $css_feature_selector = $css_feature_selector['root'] ?? null;
3480 - }
3481 - $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
3482 4266 $block_rules .= $this->process_blocks_custom_css( $node['css'], $css_selector );
3483 4267 }
3484 4268
4269 + // 8. Wrap the entire block output in a media query if this is a responsive node.
4270 + // Responsive nodes are created by get_block_nodes() for each breakpoint and carry
4271 + // a 'media_query' key.
4272 + if ( $media_query && ! empty( $block_rules ) ) {
4273 + $block_rules = $media_query . '{' . $block_rules . '}';
4274 + }
4275 +
3485 4276 return $block_rules;
3486 4277 }
3487 4278
3488 4279 /**
@@ -3896,9 +4687,9 @@
3896 4687 * Gets a `default`'s preset name by a provided slug.
3897 4688 *
3898 4689 * @since 5.9.0
3899 4690 *
3900 - * @param string $slug The slug we want to find a match from default presets.
4691 + * @param string $slug The slug we want to find a match from default presets.
3901 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
3902 4693 * @return string|null
3903 4694 */
3904 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -3946,10 +4737,10 @@
3946 4737 * @since 5.9.0
3947 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
3948 4739 *
3949 4740 * @param array $theme_json Structure to sanitize.
3950 - * @param string $origin Optional. What source of data this object represents.
3951 - * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
4741 + * @param string $origin Optional. What source of data this object represents.
4742 + * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
3952 4743 * @return array Sanitized structure.
3953 4744 */
3954 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
3955 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -3958,8 +4749,11 @@
3958 4749
3959 4750 $sanitized = array();
3960 4751
3961 4752 $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
4753 + if ( isset( $theme_json['styles'] ) ) {
4754 + $theme_json['styles'] = gutenberg_resolve_style_state_aliases( $theme_json['styles'] );
4755 + }
3962 4756
3963 4757 $blocks_metadata = static::get_blocks_metadata();
3964 4758 $valid_block_names = array_keys( $blocks_metadata );
3965 4759 $valid_element_names = array_keys( static::ELEMENTS );
@@ -3966,11 +4760,12 @@
3966 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
3967 4761
3968 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
3969 4763
3970 - $blocks_metadata = static::get_blocks_metadata();
3971 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
3972 - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4764 + $blocks_metadata = static::get_blocks_metadata();
4765 + $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4766 + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4767 + $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
3973 4768
3974 4769 foreach ( $style_nodes as $metadata ) {
3975 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
3976 4771 if ( empty( $input ) ) {
@@ -3976,8 +4771,12 @@
3976 4771 if ( empty( $input ) ) {
3977 4772 continue;
3978 4773 }
3979 4774
4775 + $block_name = in_array( 'blocks', $metadata['path'], true )
4776 + ? static::get_block_name_from_metadata_path( $metadata )
4777 + : null;
4778 +
3980 4779 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
3981 4780 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
3982 4781 $output = $input;
3983 4782 } else {
@@ -4001,8 +4800,36 @@
4001 4800 }
4002 4801 }
4003 4802 }
4004 4803
4804 + // Re-add and process responsive breakpoint styles.
4805 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4806 + if ( isset( $input[ $breakpoint ] ) ) {
4807 + $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4808 +
4809 + if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4810 + $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4811 + }
4812 +
4813 + if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4814 + $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4815 + }
4816 +
4817 + if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4818 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4819 + if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) {
4820 + $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] );
4821 + }
4822 + }
4823 + }
4824 +
4825 + // Responsive custom CSS is allowed for users with 'edit_css' capability.
4826 + if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4827 + $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css'];
4828 + }
4829 + }
4830 + }
4831 +
4005 4832 if ( ! empty( $output ) ) {
4006 4833 _wp_array_set( $sanitized, $metadata['path'], $output );
4007 4834 }
4008 4835
@@ -4015,15 +4842,43 @@
4015 4842
4016 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4017 4844
4018 4845 if ( isset( $variation_input['blocks'] ) ) {
4019 - $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] );
4846 + $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'], $responsive_media_queries );
4020 4847 }
4021 4848
4022 4849 if ( isset( $variation_input['elements'] ) ) {
4023 - $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] );
4850 + $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'], $responsive_media_queries );
4024 4851 }
4025 4852
4853 + // Re-add and process responsive breakpoint styles for variations.
4854 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4855 + if ( isset( $variation_input[ $breakpoint ] ) ) {
4856 + $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4857 +
4858 + if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4859 + $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4860 + }
4861 +
4862 + if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4863 + $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4864 + }
4865 +
4866 + if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4867 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4868 + if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4869 + $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] );
4870 + }
4871 + }
4872 + }
4873 +
4874 + // Responsive custom CSS is allowed for users with 'edit_css' capability.
4875 + if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4876 + $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css'];
4877 + }
4878 + }
4879 + }
4880 +
4026 4881 if ( ! empty( $variation_output ) ) {
4027 4882 _wp_array_set( $sanitized, $variation['path'], $variation_output );
4028 4883 }
4029 4884 }
@@ -4036,9 +4891,9 @@
4036 4891 if ( empty( $input ) ) {
4037 4892 continue;
4038 4893 }
4039 4894
4040 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4041 4896 if ( ! empty( $output ) ) {
4042 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4043 4898 }
4044 4899 }
@@ -4060,14 +4915,19 @@
4060 4915
4061 4916 /**
4062 4917 * Remove insecure element styles within a variation or block.
4063 4918 *
4919 + * When responsive media queries are provided, nested responsive state styles
4920 + * for those media-query keys are re-added after the base sanitization pass.
4921 + *
4064 4922 * @since 6.8.0
4065 4923 *
4066 - * @param array $elements The elements to process.
4924 + * @param array $elements The elements to process.
4925 + * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4926 + * viewport states. Default null.
4067 4927 * @return array The sanitized elements styles.
4068 4928 */
4069 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4070 4930 $sanitized = array();
4071 4931 $valid_element_names = array_keys( static::ELEMENTS );
4072 4932
4073 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4082,8 +4942,25 @@
4082 4942 }
4083 4943 }
4084 4944 }
4085 4945
4946 + if ( null !== $responsive_media_queries ) {
4947 + // Re-add and process responsive breakpoint styles for elements.
4948 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4949 + if ( isset( $element_input[ $breakpoint ] ) ) {
4950 + $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4951 +
4952 + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4953 + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4954 + if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4955 + $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4956 + }
4957 + }
4958 + }
4959 + }
4960 + }
4961 + }
4962 +
4086 4963 $sanitized[ $element_name ] = $element_output;
4087 4964 }
4088 4965 }
4089 4966 return $sanitized;
@@ -4091,22 +4968,44 @@
4091 4968
4092 4969 /**
4093 4970 * Remove insecure styles from inner blocks and their elements.
4094 4971 *
4972 + * When responsive media queries are provided, nested responsive state styles
4973 + * for those media-query keys are re-added after the base sanitization pass.
4974 + *
4095 4975 * @since 6.8.0
4096 4976 *
4097 - * @param array $blocks The block styles to process.
4977 + * @param array $blocks The block styles to process.
4978 + * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4979 + * viewport states. Default null.
4098 4980 * @return array Sanitized block type styles.
4099 4981 */
4100 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4101 4983 $sanitized = array();
4102 4984 foreach ( $blocks as $block_type => $block_input ) {
4103 4985 $block_output = static::remove_insecure_styles( $block_input );
4104 4986
4105 4987 if ( isset( $block_input['elements'] ) ) {
4106 - $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] );
4988 + $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'], $responsive_media_queries );
4107 4989 }
4108 4990
4991 + if ( null !== $responsive_media_queries ) {
4992 + // Re-add and process responsive breakpoint styles for inner blocks.
4993 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4994 + if ( isset( $block_input[ $breakpoint ] ) ) {
4995 + $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4996 +
4997 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4998 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4999 + if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
5000 + $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
5001 + }
5002 + }
5003 + }
5004 + }
5005 + }
5006 + }
5007 +
4109 5008 $sanitized[ $block_type ] = $block_output;
4110 5009 }
4111 5010 return $sanitized;
4112 5011 }
@@ -4145,12 +5044,14 @@
4145 5044 * without the insecure settings.
4146 5045 *
4147 5046 * @since 5.9.0
4148 5047 *
4149 - * @param array $input Node to process.
5048 + * @param array $input Node to process.
5049 + * @param bool $allow_viewport Whether to preserve and sanitize top-level
5050 + * viewport settings.
4150 5051 * @return array
4151 5052 */
4152 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4153 5054 $output = array();
4154 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4155 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4156 5057 $path_with_origin = $preset_metadata['path'];
@@ -4201,8 +5102,12 @@
4201 5102
4202 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4203 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4204 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4205 5110 return $output;
4206 5111 }
4207 5112
4208 5113 /**
@@ -4879,8 +5784,9 @@
4879 5784 * This is used to convert the internal representation of variables to the CSS representation.
4880 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
4881 5786 *
4882 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
4883 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
4884 5790 * @return string The converted variable.
4885 5791 */
4886 5792 private static function convert_custom_properties( $value ) {
@@ -4887,15 +5793,32 @@
4887 5793 $prefix = 'var:';
4888 5794 $prefix_len = strlen( $prefix );
4889 5795 $token_in = '|';
4890 5796 $token_out = '--';
4891 - if ( 0 === strpos( $value, $prefix ) ) {
4892 - $unwrapped_name = str_replace(
4893 - $token_in,
4894 - $token_out,
4895 - substr( $value, $prefix_len )
4896 - );
4897 - $value = "var(--wp--$unwrapped_name)";
5797 + if ( str_starts_with( $value, $prefix ) ) {
5798 + $parts = explode( $token_in, substr( $value, $prefix_len ) );
5799 +
5800 + /*
5801 + * The slug of a preset reference is kebab-cased so the resulting
5802 + * custom property matches the one generated from the preset,
5803 + * whose slug is also kebab-cased (see `get_settings_values_by_slug()`).
5804 + * For slugs that are not already kebab-cased (e.g. `n27`), a verbatim
5805 + * conversion produces a reference to a custom property that does
5806 + * not exist (`--wp--preset--font-family--n27` instead of the
5807 + * generated `--wp--preset--font-family--n-27`).
5808 + *
5809 + * Duotone is the exception: its custom properties are generated by
5810 + * `WP_Duotone_Gutenberg` from the presets it registers in
5811 + * `get_all_global_styles_presets()`. Duotone references are
5812 + * kebab-cased all the same: the editor and the JS style engine
5813 + * kebab-case the references of every preset type, and
5814 + * `WP_Duotone_Gutenberg` looks up presets by kebab-cased filter ID.
5815 + */
5816 + if ( 3 === count( $parts ) && 'preset' === $parts[0] ) {
5817 + $parts[2] = _wp_to_kebab_case( $parts[2] );
5818 + }
5819 +
5820 + $value = 'var(--wp--' . implode( $token_out, $parts ) . ')';
4898 5821 }
4899 5822
4900 5823 return $value;
4901 5824 }
@@ -4904,9 +5827,9 @@
4904 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
4905 5828 * It is recursive and modifies the input in-place.
4906 5829 *
4907 5830 * @since 6.3.0
4908 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
4909 5832 * @return array The modified $tree.
4910 5833 */
4911 5834 private static function resolve_custom_css_format( $tree ) {
4912 5835 $prefix = 'var:';
@@ -4911,9 +5834,9 @@
4911 5834 private static function resolve_custom_css_format( $tree ) {
4912 5835 $prefix = 'var:';
4913 5836
4914 5837 foreach ( $tree as $key => $data ) {
4915 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
4916 5839 $tree[ $key ] = self::convert_custom_properties( $data );
4917 5840 } elseif ( is_array( $data ) ) {
4918 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
4919 5842 }
@@ -5016,16 +5939,27 @@
5016 5939 return $variation_class;
5017 5940 }
5018 5941
5019 5942 $limit = 1;
5020 - $selector_parts = explode( ',', $block_selector );
5943 + $selector_parts = static::split_selector_list( $block_selector );
5021 5944 $result = array();
5022 5945
5946 + /*
5947 + * Append the variation class to each selector's ancestor: the first
5948 + * run of characters before any combinator (whitespace) or pseudo-class
5949 + * (`:`). Only the first match is replaced.
5950 + *
5951 + * Examples ("custom" variation):
5952 + * - `.wp-block` => `.wp-block.is-style-custom`
5953 + * - `.wp-block .inner` => `.wp-block.is-style-custom .inner`
5954 + * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)`
5955 + * - `:where(.outer .inner)` => `:where(.outer.is-style-custom .inner)`
5956 + */
5023 5957 foreach ( $selector_parts as $part ) {
5024 5958 $result[] = preg_replace_callback(
5025 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5026 5960 function ( $matches ) use ( $variation_class ) {
5027 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5028 5962 },
5029 5963 $part,
5030 5964 $limit
5031 5965 );
@@ -5030,9 +5964,52 @@
5030 5964 $limit
5031 5965 );
5032 5966 }
5033 5967
5034 - return implode( ',', $result );
5968 + return implode( ', ', $result );
5969 + }
5970 +
5971 + /**
5972 + * Applies a block style variation class to a feature selector.
5973 + *
5974 + * Feature selectors can target a different element than the block's root
5975 + * selector. For example, the Button block's root selector targets the inner
5976 + * link, while its dimensions width selector targets the outer wrapper. Apply
5977 + * the variation class directly to the selector that will receive the
5978 + * declarations instead of deriving it by subtracting the root selector from
5979 + * the feature selector.
5980 + *
5981 + * @param array $style_variation Style variation metadata.
5982 + * @param string $feature_selector CSS selector for the feature.
5983 + * @return string Feature selector with block style variation selector added.
5984 + */
5985 + protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
5986 + $variation_path = $style_variation['path'] ?? array();
5987 + $variation_name = $style_variation['name'] ?? ( is_array( $variation_path ) ? end( $variation_path ) : null );
5988 +
5989 + if ( ! $variation_name ) {
5990 + return $style_variation['selector'] ?? $feature_selector;
5991 + }
5992 +
5993 + $variation_class = ".is-style-$variation_name";
5994 + $selector_parts = static::split_selector_list( $feature_selector );
5995 + $selector_parts = array_map(
5996 + static function ( $selector ) use ( $variation_class ) {
5997 + $prefix = $variation_class . ' ';
5998 +
5999 + if ( str_starts_with( $selector, $prefix ) ) {
6000 + return substr( $selector, strlen( $prefix ) );
6001 + }
6002 +
6003 + return $selector;
6004 + },
6005 + $selector_parts
6006 + );
6007 +
6008 + return static::get_block_style_variation_selector(
6009 + $variation_name,
6010 + implode( ', ', $selector_parts )
6011 + );
5035 6012 }
5036 6013
5037 6014 /**
5038 6015 * Collects valid block style variations keyed by block type.