PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/class-wp-theme-json-gutenberg.php +1152 -179 23.1.0 → trunk View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -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.