PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
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 +1152 -179 23.1.0 → 24.1.0 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 */
@@ -305,8 +315,9 @@
305 315 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 316 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 317 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 318 'text-decoration' => array( 'typography', 'textDecoration' ),
319 + 'text-shadow' => array( 'typography', 'textShadow' ),
309 320 'text-transform' => array( 'typography', 'textTransform' ),
310 321 'text-indent' => array( 'typography', 'textIndent' ),
311 322 'filter' => array( 'filter', 'duotone' ),
312 323 'box-shadow' => array( 'shadow' ),
@@ -388,10 +399,13 @@
388 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
389 400 * @since 6.4.0 Added `layout.allowEditing`.
390 401 * @since 6.4.0 Added `lightbox`.
391 402 * @since 7.0.0 Added type markers to the schema for boolean values.
392 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
393 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`.
394 408 * @var array
395 409 */
396 410 const VALID_SETTINGS = array(
397 411 'appearanceTools' => null,
@@ -400,8 +414,11 @@
400 414 'backgroundImage' => null,
401 415 'backgroundSize' => null,
402 416 'gradient' => null,
403 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
404 421 'border' => array(
405 422 'color' => null,
406 423 'radius' => null,
407 424 'style' => null,
@@ -464,25 +481,32 @@
464 481 'presets' => null,
465 482 'defaultPresets' => null,
466 483 ),
467 484 'typography' => array(
468 - 'fluid' => null,
469 - 'customFontSize' => null,
470 - 'defaultFontSizes' => null,
471 - 'dropCap' => null,
472 - 'fontFamilies' => null,
473 - 'fontSizes' => null,
474 - 'fontStyle' => null,
475 - 'fontWeight' => null,
476 - 'letterSpacing' => null,
477 - 'lineHeight' => null,
478 - 'textAlign' => null,
479 - 'textColumns' => null,
480 - 'textDecoration' => null,
481 - 'textIndent' => null,
482 - 'textTransform' => null,
483 - '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,
484 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
485 509 );
486 510
487 511 const FONT_FAMILY_SCHEMA = array(
488 512 array(
@@ -521,9 +545,9 @@
521 545 * updated `blockGap` to be allowed at any level.
522 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
523 547 * @since 6.6.0 Added `background` sub properties to top-level only.
524 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
525 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
526 550 * `typography.textIndent` properties.
527 551 * @var array
528 552 */
529 553 const VALID_STYLES = array(
@@ -582,8 +606,9 @@
582 606 'textAlign' => null,
583 607 'textColumns' => null,
584 608 'textDecoration' => null,
585 609 'textIndent' => null,
610 + 'textShadow' => null,
586 611 'textTransform' => null,
587 612 'writingMode' => null,
588 613 ),
589 614 'css' => null,
@@ -620,16 +645,187 @@
620 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
621 646 );
622 647
623 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 + /**
624 820 * Custom states for blocks that map to CSS class selectors rather than
625 - * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
626 - * 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.
627 823 *
628 824 * The CSS selector for each state is defined in the block's block.json
629 825 * under `selectors.states`, e.g.:
630 826 *
631 - * "selectors": { "states": { "@current": ".some-css-selector" } }
827 + * "selectors": { "states": { "-current": ".some-css-selector" } }
632 828 *
633 829 * This constant controls which states are valid in theme.json for a given
634 830 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
635 831 * as valid sub-states, producing compound selectors such as
@@ -637,9 +833,9 @@
637 833 *
638 834 * @var array
639 835 */
640 836 const VALID_BLOCK_CUSTOM_STATES = array(
641 - 'core/navigation-link' => array( '@current' ),
837 + 'core/navigation-link' => array( '-current' ),
642 838 );
643 839
644 840 /**
645 841 * The valid elements that can be found under styles.
@@ -661,8 +857,9 @@
661 857 'button' => '.wp-element-button, .wp-block-button__link',
662 858 // The block classes are necessary to target older content that won't use the new class names.
663 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',
664 860 'cite' => 'cite',
861 + 'label' => 'label',
665 862 'select' => 'select',
666 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
667 864 );
668 865
@@ -734,17 +931,35 @@
734 931
735 932 /**
736 933 * Processes pseudo-selectors for any node (block or variation).
737 934 *
738 - * @param array $node The node data (block or variation).
739 - * @param string $base_selector The base selector.
740 - * @param array $settings The theme settings.
741 - * @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.
742 943 * @return array Array of pseudo-selector declarations.
743 944 */
744 - 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 ) {
745 946 $pseudo_declarations = array();
947 + $add_declarations = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
948 + if ( empty( $declarations ) ) {
949 + return;
950 + }
746 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 +
747 962 if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
748 963 return $pseudo_declarations;
749 964 }
750 965
@@ -749,11 +964,28 @@
749 964 }
750 965
751 966 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
752 967 if ( isset( $node[ $pseudo_selector ] ) ) {
753 - $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
754 - $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
755 - $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 );
756 988 }
757 989 }
758 990
759 991 return $pseudo_declarations;
@@ -841,9 +1073,12 @@
841 1073 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
842 1074 $origin = 'theme';
843 1075 }
844 1076
845 - $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 + }
846 1081 $blocks_metadata = static::get_blocks_metadata();
847 1082 $valid_block_names = array_keys( $blocks_metadata );
848 1083 $valid_element_names = array_keys( static::ELEMENTS );
849 1084 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
@@ -1012,8 +1247,10 @@
1012 1247 *
1013 1248 * @since 5.8.0
1014 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1015 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.
1016 1253 *
1017 1254 * @param array $input Structure to sanitize.
1018 1255 * @param array $valid_block_names List of valid block names.
1019 1256 * @param array $valid_element_names List of valid element names.
@@ -1048,17 +1285,19 @@
1048 1285 }
1049 1286 }
1050 1287
1051 1288 // Build the schema based on valid block & element names.
1052 - $schema = array();
1053 - $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 );
1054 1292
1055 1293 /*
1056 - * Set allowed element pseudo selectors based on per element allow list.
1294 + * Set allowed element pseudo selectors and responsive breakpoint states.
1057 1295 * Target data structure in schema:
1058 1296 * e.g.
1059 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1060 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']`.
1061 1300 */
1062 1301 foreach ( $valid_element_names as $element ) {
1063 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1064 1303
@@ -1066,28 +1305,62 @@
1066 1305 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
1067 1306 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
1068 1307 }
1069 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 + }
1070 1314 }
1071 1315
1072 1316 $schema_styles_blocks = array();
1073 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1074 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 +
1075 1327 /*
1076 1328 * Generate a schema for blocks.
1077 - * - Block styles can contain `elements` & `variations` definitions.
1329 + * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1078 1330 * - Variations definitions cannot be nested.
1079 - * - Variations can contain styles for inner `blocks`.
1080 - * - 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.
1081 1333 *
1082 - * As each variation needs a `blocks` schema but further nested
1083 - * 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.
1084 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 +
1085 1350 foreach ( $valid_block_names as $block ) {
1086 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1087 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1088 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1089 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 +
1090 1363 // Add pseudo-selectors for blocks that support them.
1091 1364 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1092 1365 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1093 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1093,9 +1366,9 @@
1093 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1094 1367 }
1095 1368 }
1096 1369
1097 - // 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).
1098 1371 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1099 1372 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1100 1373 $custom_state_schema = $styles_non_top_level;
1101 1374 // The same pseudo-selectors valid for the block at the top level
@@ -1132,8 +1405,20 @@
1132 1405 if ( ! empty( $style_variation_names ) ) {
1133 1406 foreach ( $style_variation_names as $variation_name ) {
1134 1407 $variation_schema = $block_style_variation_styles;
1135 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 +
1136 1421 // Add pseudo-selectors to variations for blocks that support them.
1137 1422 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1138 1423 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1139 1424 $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
@@ -1153,8 +1438,49 @@
1153 1438 $schema['settings'] = static::VALID_SETTINGS;
1154 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1155 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1156 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 +
1157 1483 // Remove anything that's not present in the schema.
1158 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1159 1485 if ( ! isset( $input[ $subtree ] ) ) {
1160 1486 continue;
@@ -1166,8 +1492,12 @@
1166 1492 }
1167 1493
1168 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1169 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1170 1500 if ( empty( $result ) ) {
1171 1501 unset( $output[ $subtree ] );
1172 1502 } else {
1173 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1195,14 +1525,38 @@
1195 1525 protected static function append_to_selector( $selector, $to_append ) {
1196 1526 if ( ! str_contains( $selector, ',' ) ) {
1197 1527 return $selector . $to_append;
1198 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 +
1199 1553 $new_selectors = array();
1200 - $selectors = explode( ',', $selector );
1554 + $selectors = static::split_selector_list( $selector );
1201 1555 foreach ( $selectors as $sel ) {
1202 1556 $new_selectors[] = $sel . $to_append;
1203 1557 }
1204 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1205 1559 }
1206 1560
1207 1561 /**
1208 1562 * Prepends a sub-selector to an existing one.
@@ -1220,17 +1574,199 @@
1220 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1221 1575 if ( ! str_contains( $selector, ',' ) ) {
1222 1576 return $to_prepend . $selector;
1223 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 +
1224 1602 $new_selectors = array();
1225 - $selectors = explode( ',', $selector );
1603 + $selectors = static::split_selector_list( $selector );
1226 1604 foreach ( $selectors as $sel ) {
1227 1605 $new_selectors[] = $to_prepend . $sel;
1228 1606 }
1229 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1230 1609 }
1231 1610
1232 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 + /**
1233 1769 * Returns the metadata for each block.
1234 1770 *
1235 1771 * Example:
1236 1772 *
@@ -1647,9 +2183,9 @@
1647 2183 /**
1648 2184 * Returns the global styles custom CSS for a single block.
1649 2185 * This function is deprecated; please do not sync to core.
1650 2186 *
1651 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1652 2188 * @param string $selector The block selector.
1653 2189 *
1654 2190 * @return string The global styles custom CSS for the block.
1655 2191 */
@@ -1774,9 +2310,10 @@
1774 2310
1775 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1776 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1777 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1778 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1779 2316 // Use a fallback gap value if block gap support is not available.
1780 2317 if ( ! $has_block_gap_support ) {
1781 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1782 2319 if ( ! empty( $block_type ) ) {
@@ -1784,18 +2321,29 @@
1784 2321 }
1785 2322 } else {
1786 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1787 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1788 2326
1789 2327 // Support split row / column values and concatenate to a shorthand value.
1790 2328 if ( is_array( $block_gap_value ) ) {
1791 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1792 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1793 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1794 - $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;
1795 2342 } else {
1796 - // Skip outputting gap value if not all sides are provided.
1797 - $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;
1798 2346 }
1799 2347 }
1800 2348
1801 2349 // If the block should have custom gap, add the gap styles.
@@ -1805,10 +2353,13 @@
1805 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1806 2354 continue;
1807 2355 }
1808 2356
1809 - $class_name = $layout_definition['className'] ?? false;
1810 - $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;
1811 2362
1812 2363 if (
1813 2364 ! empty( $class_name ) &&
1814 2365 ! empty( $spacing_rules )
@@ -1821,9 +2372,9 @@
1821 2372 ! empty( $spacing_rule['rules'] )
1822 2373 ) {
1823 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1824 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1825 - $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;
1826 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1827 2378 $declarations[] = array(
1828 2379 'name' => $css_property,
1829 2380 'value' => $current_css_value,
@@ -1935,8 +2486,13 @@
1935 2486 }
1936 2487 }
1937 2488 }
1938 2489 }
2490 +
2491 + if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) {
2492 + $block_rules = $options['media_query'] . '{' . $block_rules . '}';
2493 + }
2494 +
1939 2495 return $block_rules;
1940 2496 }
1941 2497
1942 2498 /**
@@ -1957,9 +2513,9 @@
1957 2513 * .has-value-gradient-background {
1958 2514 * background: value;
1959 2515 * }
1960 2516 *
1961 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
1962 2518 * background: value;
1963 2519 * }
1964 2520 *
1965 2521 * @since 5.9.0
@@ -2160,10 +2716,18 @@
2160 2716 foreach ( $slugs as $slug ) {
2161 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2162 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2163 2719
2164 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2165 - $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;
2166 2730 $stylesheet .= static::to_ruleset(
2167 2731 $new_selector,
2168 2732 array(
2169 2733 array(
@@ -2200,16 +2764,14 @@
2200 2764 if ( ! $scope || ! $selector ) {
2201 2765 return $selector;
2202 2766 }
2203 2767
2204 - $scopes = explode( ',', $scope );
2205 - $selectors = explode( ',', $selector );
2768 + $scopes = static::split_selector_list( $scope );
2769 + $selectors = static::split_selector_list( $selector );
2206 2770
2207 2771 $selectors_scoped = array();
2208 2772 foreach ( $scopes as $outer ) {
2209 2773 foreach ( $selectors as $inner ) {
2210 - $outer = trim( $outer );
2211 - $inner = trim( $inner );
2212 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2213 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2214 2776 } elseif ( empty( $outer ) ) {
2215 2777 $selectors_scoped[] = $inner;
@@ -2515,15 +3077,15 @@
2515 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2516 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.
2517 3079 * @since 6.7.0 Allow ref resolution of background properties.
2518 3080 *
2519 - * @param array $styles Styles to process.
2520 - * @param array $settings Theme settings.
2521 - * @param array $properties Properties metadata.
2522 - * @param array $theme_json Theme JSON array.
2523 - * @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.
2524 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2525 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2526 3088 */
2527 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2528 3090 if ( empty( $styles ) ) {
2529 3091 return array();
@@ -2647,10 +3209,10 @@
2647 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2648 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2649 3211 * @since 6.7.0 Added support for background image refs
2650 3212 *
2651 - * @param array $styles Styles subtree.
2652 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2653 3215 * @param array $theme_json Theme JSON array.
2654 3216 * @return string|array Style property value.
2655 3217 */
2656 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3022,9 +3584,9 @@
3022 3584 * @since 6.1.0
3023 3585 *
3024 3586 * @param array $theme_json The theme.json converted to an array.
3025 3587 * @param array $selectors Optional list of selectors per block.
3026 - * @param array $options {
3588 + * @param array $options {
3027 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3028 3590 *
3029 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3030 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3037,10 +3599,11 @@
3037 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3038 3600 return $nodes;
3039 3601 }
3040 3602
3041 - $include_variations = $options['include_block_style_variations'] ?? false;
3042 - $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 );
3043 3606
3044 3607 // If only node paths are to be returned, skip selector assignment.
3045 3608 if ( ! $include_node_paths_only ) {
3046 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3084,8 +3647,9 @@
3084 3647
3085 3648 if ( $include_variations && isset( $node['variations'] ) ) {
3086 3649 foreach ( $node['variations'] as $variation => $node ) {
3087 3650 $variation_selectors[] = array(
3651 + 'name' => $variation,
3088 3652 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3089 3653 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3090 3654 );
3091 3655 }
@@ -3095,49 +3659,100 @@
3095 3659 'name' => $name,
3096 3660 'path' => $node_path,
3097 3661 'selector' => $selector,
3098 3662 'selectors' => $feature_selectors,
3663 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3099 3664 'duotone' => $duotone_selector,
3100 3665 'variations' => $variation_selectors,
3101 3666 'css' => $selector,
3102 3667 );
3103 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 +
3104 3687 // Handle any pseudo selectors for the block.
3105 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3106 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3107 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) {
3108 - /*
3109 - * Append the pseudo-selector to each feature selector so that
3110 - * get_feature_declarations_for_node generates CSS scoped to the
3111 - * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3112 - * default state (e.g. '.wp-block-button').
3113 - */
3114 - $pseudo_feature_selectors = array();
3115 - foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3116 - if ( is_array( $feature_selector ) ) {
3117 - $pseudo_feature_selectors[ $feature ] = array();
3118 - foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3119 - $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3120 - }
3121 - } else {
3122 - $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 );
3123 3715 }
3716 + } else {
3717 + $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3124 3718 }
3719 + }
3125 3720
3721 + if ( $has_pseudo ) {
3126 3722 $nodes[] = array(
3127 3723 'name' => $name,
3128 3724 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ),
3129 3725 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3130 3726 'selectors' => $pseudo_feature_selectors,
3727 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3131 3728 'duotone' => $duotone_selector,
3132 3729 'variations' => $variation_selectors,
3133 3730 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3134 3731 );
3135 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 + }
3136 3751 }
3137 3752 }
3138 3753
3139 - // Handle custom states (e.g. '@current' for navigation).
3754 + // Handle custom states (e.g. '-current' for navigation).
3140 3755 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3141 3756 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3142 3757 if (
3143 3758 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
@@ -3148,8 +3763,9 @@
3148 3763 'name' => $name,
3149 3764 'path' => array( 'styles', 'blocks', $name, $custom_state ),
3150 3765 'selector' => $custom_css_selector,
3151 3766 'selectors' => $feature_selectors,
3767 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3152 3768 'duotone' => $duotone_selector,
3153 3769 'variations' => $variation_selectors,
3154 3770 'css' => $custom_css_selector,
3155 3771 );
@@ -3163,8 +3779,9 @@
3163 3779 'name' => $name,
3164 3780 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
3165 3781 'selector' => $compound_css_selector,
3166 3782 'selectors' => $feature_selectors,
3783 + 'elements' => $selectors[ $name ]['elements'] ?? array(),
3167 3784 'duotone' => $duotone_selector,
3168 3785 'variations' => $variation_selectors,
3169 3786 'css' => $compound_css_selector,
3170 3787 );
@@ -3174,39 +3791,84 @@
3174 3791 }
3175 3792 }
3176 3793 }
3177 3794 }
3178 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3179 - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3180 - $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 );
3181 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 ] ) ) {
3182 3829 $nodes[] = array(
3183 - 'path' => $node_path,
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3184 3832 );
3185 - continue;
3186 3833 }
3187 3834
3188 - $nodes[] = array(
3189 - 'path' => $node_path,
3190 - 'selector' => $selectors[ $name ]['elements'][ $element ],
3191 - );
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 + }
3192 3846
3193 3847 // Handle any pseudo selectors for the element.
3194 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3195 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.
3196 3853 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3197 - $node_path = array( 'styles', 'blocks', $name, 'elements', $element );
3198 - 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 ] ) ) {
3199 3865 $nodes[] = array(
3200 - '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 ],
3201 3869 );
3202 - continue;
3203 3870 }
3204 -
3205 - $nodes[] = array(
3206 - 'path' => $node_path,
3207 - 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
3208 - );
3209 3871 }
3210 3872 }
3211 3873 }
3212 3874 }
@@ -3226,13 +3888,15 @@
3226 3888 *
3227 3889 * @return string Styles for the block.
3228 3890 */
3229 3891 public function get_styles_for_block( $block_metadata ) {
3230 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3231 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3232 - $selector = $block_metadata['selector'];
3233 - $settings = $this->theme_json['settings'] ?? null;
3234 - $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 );
3235 3899
3236 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3237 3901
3238 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3237,20 +3901,24 @@
3237 3901
3238 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
3239 3903 $block_name = $block_metadata['name'] ?? null;
3240 3904 $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
3905 + $block_elements = $block_metadata['elements'] ?? array();
3241 3906
3242 3907 // Update button width declarations for percentage values to use calc() with block gap.
3243 3908 $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
3244 3909
3245 3910 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3246 - $style_variation_declarations = array();
3247 - $style_variation_custom_css = array();
3248 - $style_variation_layout_metadata = array();
3249 - 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'] ) ) {
3250 3919 foreach ( $block_metadata['variations'] as $style_variation ) {
3251 - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3252 - $clean_style_variation_selector = trim( $style_variation['selector'] );
3920 + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3253 3921
3254 3922 // Generate any feature/subfeature style declarations for the current style variation.
3255 3923 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3256 3924
@@ -3261,27 +3929,10 @@
3261 3929 $variation_declarations = static::update_button_width_declarations( $variation_declarations, $settings );
3262 3930
3263 3931 // Combine selectors with style variation's selector and add to overall style variation declarations.
3264 3932 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3265 - /*
3266 - * Clean up any whitespace between comma separated selectors.
3267 - * This prevents these spaces breaking compound selectors such as:
3268 - * - `.wp-block-list:not(.wp-block-list .wp-block-list)`
3269 - * - `.wp-block-image img, .wp-block-image.my-class img`
3270 - */
3271 - $clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
3272 - $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector );
3933 + $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $current_selector );
3273 3934
3274 - // Prepend the variation selector to the current selector.
3275 - $split_selectors = explode( ',', $shortened_selector );
3276 - $updated_selectors = array_map(
3277 - static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3278 - return $clean_style_variation_selector . $split_selector;
3279 - },
3280 - $split_selectors
3281 - );
3282 - $combined_selectors = implode( ',', $updated_selectors );
3283 -
3284 3935 // Add the new declarations to the overall results under the modified selector.
3285 3936 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3286 3937 }
3287 3938 // Compute declarations for remaining styles not covered by feature level selectors.
@@ -3288,9 +3939,9 @@
3288 3939 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3289 3940
3290 3941 // Process pseudo-selectors for this variation (e.g., :hover, :focus).
3291 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 );
3292 - $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 );
3293 3944 $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3294 3945
3295 3946 // Store custom CSS for the style variation.
3296 3947 if ( isset( $style_variation_node['css'] ) ) {
@@ -3300,18 +3951,135 @@
3300 3951 // Store variation metadata and node for layout styles generation.
3301 3952 // Only store if the variation has blockGap defined.
3302 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3303 3954 // Append block selector to the variation selector for proper targeting.
3304 - $variation_metadata_with_selector = $style_variation;
3305 - $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 +
3306 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3307 3968 'metadata' => $variation_metadata_with_selector,
3308 3969 'node' => $style_variation_node,
3309 3970 );
3310 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 + }
3311 4080 }
3312 4081 }
3313 -
3314 4082 /*
3315 4083 * Get a reference to element name from path.
3316 4084 * $block_metadata['path'] = array( 'styles','elements','link' );
3317 4085 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
@@ -3473,20 +4241,39 @@
3473 4241 }
3474 4242 if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
3475 4243 $block_rules .= $style_variation_custom_css[ $style_variation_selector ];
3476 4244 }
4245 + if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) {
4246 + $block_rules .= $style_variation_responsive_css[ $style_variation_selector ];
4247 + }
3477 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 + }
3478 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 +
3479 4264 // 7. Generate and append any custom CSS rules.
3480 4265 if ( isset( $node['css'] ) && ! $is_root_selector ) {
3481 - $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
3482 - if ( is_array( $css_feature_selector ) ) {
3483 - $css_feature_selector = $css_feature_selector['root'] ?? null;
3484 - }
3485 - $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
3486 4266 $block_rules .= $this->process_blocks_custom_css( $node['css'], $css_selector );
3487 4267 }
3488 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 +
3489 4276 return $block_rules;
3490 4277 }
3491 4278
3492 4279 /**
@@ -3900,9 +4687,9 @@
3900 4687 * Gets a `default`'s preset name by a provided slug.
3901 4688 *
3902 4689 * @since 5.9.0
3903 4690 *
3904 - * @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.
3905 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
3906 4693 * @return string|null
3907 4694 */
3908 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -3950,10 +4737,10 @@
3950 4737 * @since 5.9.0
3951 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
3952 4739 *
3953 4740 * @param array $theme_json Structure to sanitize.
3954 - * @param string $origin Optional. What source of data this object represents.
3955 - * 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'.
3956 4743 * @return array Sanitized structure.
3957 4744 */
3958 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
3959 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -3962,8 +4749,11 @@
3962 4749
3963 4750 $sanitized = array();
3964 4751
3965 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 + }
3966 4756
3967 4757 $blocks_metadata = static::get_blocks_metadata();
3968 4758 $valid_block_names = array_keys( $blocks_metadata );
3969 4759 $valid_element_names = array_keys( static::ELEMENTS );
@@ -3970,11 +4760,12 @@
3970 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
3971 4761
3972 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
3973 4763
3974 - $blocks_metadata = static::get_blocks_metadata();
3975 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
3976 - $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 );
3977 4768
3978 4769 foreach ( $style_nodes as $metadata ) {
3979 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
3980 4771 if ( empty( $input ) ) {
@@ -3980,8 +4771,12 @@
3980 4771 if ( empty( $input ) ) {
3981 4772 continue;
3982 4773 }
3983 4774
4775 + $block_name = in_array( 'blocks', $metadata['path'], true )
4776 + ? static::get_block_name_from_metadata_path( $metadata )
4777 + : null;
4778 +
3984 4779 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
3985 4780 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
3986 4781 $output = $input;
3987 4782 } else {
@@ -4005,8 +4800,36 @@
4005 4800 }
4006 4801 }
4007 4802 }
4008 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 +
4009 4832 if ( ! empty( $output ) ) {
4010 4833 _wp_array_set( $sanitized, $metadata['path'], $output );
4011 4834 }
4012 4835
@@ -4019,15 +4842,43 @@
4019 4842
4020 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4021 4844
4022 4845 if ( isset( $variation_input['blocks'] ) ) {
4023 - $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 );
4024 4847 }
4025 4848
4026 4849 if ( isset( $variation_input['elements'] ) ) {
4027 - $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 );
4028 4851 }
4029 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 +
4030 4881 if ( ! empty( $variation_output ) ) {
4031 4882 _wp_array_set( $sanitized, $variation['path'], $variation_output );
4032 4883 }
4033 4884 }
@@ -4040,9 +4891,9 @@
4040 4891 if ( empty( $input ) ) {
4041 4892 continue;
4042 4893 }
4043 4894
4044 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4045 4896 if ( ! empty( $output ) ) {
4046 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4047 4898 }
4048 4899 }
@@ -4064,14 +4915,19 @@
4064 4915
4065 4916 /**
4066 4917 * Remove insecure element styles within a variation or block.
4067 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 + *
4068 4922 * @since 6.8.0
4069 4923 *
4070 - * @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.
4071 4927 * @return array The sanitized elements styles.
4072 4928 */
4073 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4074 4930 $sanitized = array();
4075 4931 $valid_element_names = array_keys( static::ELEMENTS );
4076 4932
4077 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4086,8 +4942,25 @@
4086 4942 }
4087 4943 }
4088 4944 }
4089 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 +
4090 4963 $sanitized[ $element_name ] = $element_output;
4091 4964 }
4092 4965 }
4093 4966 return $sanitized;
@@ -4095,22 +4968,44 @@
4095 4968
4096 4969 /**
4097 4970 * Remove insecure styles from inner blocks and their elements.
4098 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 + *
4099 4975 * @since 6.8.0
4100 4976 *
4101 - * @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.
4102 4980 * @return array Sanitized block type styles.
4103 4981 */
4104 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4105 4983 $sanitized = array();
4106 4984 foreach ( $blocks as $block_type => $block_input ) {
4107 4985 $block_output = static::remove_insecure_styles( $block_input );
4108 4986
4109 4987 if ( isset( $block_input['elements'] ) ) {
4110 - $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 );
4111 4989 }
4112 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 +
4113 5008 $sanitized[ $block_type ] = $block_output;
4114 5009 }
4115 5010 return $sanitized;
4116 5011 }
@@ -4149,12 +5044,14 @@
4149 5044 * without the insecure settings.
4150 5045 *
4151 5046 * @since 5.9.0
4152 5047 *
4153 - * @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.
4154 5051 * @return array
4155 5052 */
4156 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4157 5054 $output = array();
4158 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4159 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4160 5057 $path_with_origin = $preset_metadata['path'];
@@ -4205,8 +5102,12 @@
4205 5102
4206 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4207 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4208 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4209 5110 return $output;
4210 5111 }
4211 5112
4212 5113 /**
@@ -4883,8 +5784,9 @@
4883 5784 * This is used to convert the internal representation of variables to the CSS representation.
4884 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
4885 5786 *
4886 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
4887 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
4888 5790 * @return string The converted variable.
4889 5791 */
4890 5792 private static function convert_custom_properties( $value ) {
@@ -4891,15 +5793,32 @@
4891 5793 $prefix = 'var:';
4892 5794 $prefix_len = strlen( $prefix );
4893 5795 $token_in = '|';
4894 5796 $token_out = '--';
4895 - if ( 0 === strpos( $value, $prefix ) ) {
4896 - $unwrapped_name = str_replace(
4897 - $token_in,
4898 - $token_out,
4899 - substr( $value, $prefix_len )
4900 - );
4901 - $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 ) . ')';
4902 5821 }
4903 5822
4904 5823 return $value;
4905 5824 }
@@ -4908,9 +5827,9 @@
4908 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
4909 5828 * It is recursive and modifies the input in-place.
4910 5829 *
4911 5830 * @since 6.3.0
4912 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
4913 5832 * @return array The modified $tree.
4914 5833 */
4915 5834 private static function resolve_custom_css_format( $tree ) {
4916 5835 $prefix = 'var:';
@@ -4915,9 +5834,9 @@
4915 5834 private static function resolve_custom_css_format( $tree ) {
4916 5835 $prefix = 'var:';
4917 5836
4918 5837 foreach ( $tree as $key => $data ) {
4919 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
4920 5839 $tree[ $key ] = self::convert_custom_properties( $data );
4921 5840 } elseif ( is_array( $data ) ) {
4922 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
4923 5842 }
@@ -5020,16 +5939,27 @@
5020 5939 return $variation_class;
5021 5940 }
5022 5941
5023 5942 $limit = 1;
5024 - $selector_parts = explode( ',', $block_selector );
5943 + $selector_parts = static::split_selector_list( $block_selector );
5025 5944 $result = array();
5026 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 + */
5027 5957 foreach ( $selector_parts as $part ) {
5028 5958 $result[] = preg_replace_callback(
5029 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5030 5960 function ( $matches ) use ( $variation_class ) {
5031 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5032 5962 },
5033 5963 $part,
5034 5964 $limit
5035 5965 );
@@ -5034,9 +5964,52 @@
5034 5964 $limit
5035 5965 );
5036 5966 }
5037 5967
5038 - 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 + );
5039 6012 }
5040 6013
5041 6014 /**
5042 6015 * Collects valid block style variations keyed by block type.