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 +858 -260 23.2.1 → trunk View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -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,29 +645,187 @@
620 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
621 646 );
622 647
623 648 /**
624 - * Responsive breakpoint state keys and their corresponding CSS media queries.
625 - * These are available for all blocks and wrap their styles in the given media query.
626 - * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx.
649 + * Default viewport breakpoint sizes.
627 650 *
628 651 * @since 7.1.0
629 652 * @var array
630 653 */
631 - const RESPONSIVE_BREAKPOINTS = array(
632 - 'mobile' => '@media (width <= 480px)',
633 - 'tablet' => '@media (480px < width <= 782px)',
654 + const DEFAULT_VIEWPORT_BREAKPOINTS = array(
655 + 'mobile' => '480px',
656 + 'tablet' => '782px',
634 657 );
635 658
636 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 + /**
637 820 * Custom states for blocks that map to CSS class selectors rather than
638 - * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
639 - * 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.
640 823 *
641 824 * The CSS selector for each state is defined in the block's block.json
642 825 * under `selectors.states`, e.g.:
643 826 *
644 - * "selectors": { "states": { "@current": ".some-css-selector" } }
827 + * "selectors": { "states": { "-current": ".some-css-selector" } }
645 828 *
646 829 * This constant controls which states are valid in theme.json for a given
647 830 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
648 831 * as valid sub-states, producing compound selectors such as
@@ -650,9 +833,9 @@
650 833 *
651 834 * @var array
652 835 */
653 836 const VALID_BLOCK_CUSTOM_STATES = array(
654 - 'core/navigation-link' => array( '@current' ),
837 + 'core/navigation-link' => array( '-current' ),
655 838 );
656 839
657 840 /**
658 841 * The valid elements that can be found under styles.
@@ -674,8 +857,9 @@
674 857 'button' => '.wp-element-button, .wp-block-button__link',
675 858 // The block classes are necessary to target older content that won't use the new class names.
676 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',
677 860 'cite' => 'cite',
861 + 'label' => 'label',
678 862 'select' => 'select',
679 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
680 864 );
681 865
@@ -747,17 +931,35 @@
747 931
748 932 /**
749 933 * Processes pseudo-selectors for any node (block or variation).
750 934 *
751 - * @param array $node The node data (block or variation).
752 - * @param string $base_selector The base selector.
753 - * @param array $settings The theme settings.
754 - * @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.
755 943 * @return array Array of pseudo-selector declarations.
756 944 */
757 - 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 ) {
758 946 $pseudo_declarations = array();
947 + $add_declarations = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
948 + if ( empty( $declarations ) ) {
949 + return;
950 + }
759 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 +
760 962 if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
761 963 return $pseudo_declarations;
762 964 }
763 965
@@ -762,11 +964,28 @@
762 964 }
763 965
764 966 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
765 967 if ( isset( $node[ $pseudo_selector ] ) ) {
766 - $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
767 - $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
768 - $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 );
769 988 }
770 989 }
771 990
772 991 return $pseudo_declarations;
@@ -854,9 +1073,12 @@
854 1073 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
855 1074 $origin = 'theme';
856 1075 }
857 1076
858 - $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 + }
859 1081 $blocks_metadata = static::get_blocks_metadata();
860 1082 $valid_block_names = array_keys( $blocks_metadata );
861 1083 $valid_element_names = array_keys( static::ELEMENTS );
862 1084 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
@@ -1025,8 +1247,10 @@
1025 1247 *
1026 1248 * @since 5.8.0
1027 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1028 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.
1029 1253 *
1030 1254 * @param array $input Structure to sanitize.
1031 1255 * @param array $valid_block_names List of valid block names.
1032 1256 * @param array $valid_element_names List of valid element names.
@@ -1061,10 +1285,11 @@
1061 1285 }
1062 1286 }
1063 1287
1064 1288 // Build the schema based on valid block & element names.
1065 - $schema = array();
1066 - $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 );
1067 1292
1068 1293 /*
1069 1294 * Set allowed element pseudo selectors and responsive breakpoint states.
1070 1295 * Target data structure in schema:
@@ -1070,9 +1295,9 @@
1070 1295 * Target data structure in schema:
1071 1296 * e.g.
1072 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1073 1298 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1074 - * - block responsive elements: `$schema['styles']['blocks']['core/button']['tablet']['elements']['link'][':hover']`.
1299 + * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1075 1300 */
1076 1301 foreach ( $valid_element_names as $element ) {
1077 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1078 1303
@@ -1082,9 +1307,9 @@
1082 1307 }
1083 1308 }
1084 1309
1085 1310 // Add responsive breakpoint states for elements.
1086 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1311 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1087 1312 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1088 1313 }
1089 1314 }
1090 1315
@@ -1089,9 +1314,17 @@
1089 1314 }
1090 1315
1091 1316 $schema_styles_blocks = array();
1092 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1093 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 +
1094 1327 /*
1095 1328 * Generate a schema for blocks.
1096 1329 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1097 1330 * - Variations definitions cannot be nested.
@@ -1099,20 +1332,29 @@
1099 1332 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1100 1333 *
1101 1334 * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1102 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.
1103 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 +
1104 1350 foreach ( $valid_block_names as $block ) {
1105 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1106 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1107 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1108 1353
1109 - // Add responsive breakpoint states for all blocks.
1110 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1111 - $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level;
1112 - $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1113 -
1114 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
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 ) {
1115 1357 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1116 1358 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1117 1359 }
1118 1360 }
@@ -1124,9 +1366,9 @@
1124 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1125 1367 }
1126 1368 }
1127 1369
1128 - // 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).
1129 1371 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1130 1372 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1131 1373 $custom_state_schema = $styles_non_top_level;
1132 1374 // The same pseudo-selectors valid for the block at the top level
@@ -1164,12 +1406,11 @@
1164 1406 foreach ( $style_variation_names as $variation_name ) {
1165 1407 $variation_schema = $block_style_variation_styles;
1166 1408
1167 1409 // Add responsive breakpoint states to block style variations.
1168 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1410 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1169 1411 $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1170 1412 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1171 - $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks;
1172 1413
1173 1414 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1174 1415 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1175 1416 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1197,8 +1438,49 @@
1197 1438 $schema['settings'] = static::VALID_SETTINGS;
1198 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1199 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1200 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 +
1201 1483 // Remove anything that's not present in the schema.
1202 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1203 1485 if ( ! isset( $input[ $subtree ] ) ) {
1204 1486 continue;
@@ -1210,8 +1492,12 @@
1210 1492 }
1211 1493
1212 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1213 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1214 1500 if ( empty( $result ) ) {
1215 1501 unset( $output[ $subtree ] );
1216 1502 } else {
1217 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1239,14 +1525,38 @@
1239 1525 protected static function append_to_selector( $selector, $to_append ) {
1240 1526 if ( ! str_contains( $selector, ',' ) ) {
1241 1527 return $selector . $to_append;
1242 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 +
1243 1553 $new_selectors = array();
1244 - $selectors = explode( ',', $selector );
1554 + $selectors = static::split_selector_list( $selector );
1245 1555 foreach ( $selectors as $sel ) {
1246 1556 $new_selectors[] = $sel . $to_append;
1247 1557 }
1248 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1249 1559 }
1250 1560
1251 1561 /**
1252 1562 * Prepends a sub-selector to an existing one.
@@ -1264,17 +1574,199 @@
1264 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1265 1575 if ( ! str_contains( $selector, ',' ) ) {
1266 1576 return $to_prepend . $selector;
1267 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 +
1268 1602 $new_selectors = array();
1269 - $selectors = explode( ',', $selector );
1603 + $selectors = static::split_selector_list( $selector );
1270 1604 foreach ( $selectors as $sel ) {
1271 1605 $new_selectors[] = $to_prepend . $sel;
1272 1606 }
1273 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1274 1609 }
1275 1610
1276 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 + /**
1277 1769 * Returns the metadata for each block.
1278 1770 *
1279 1771 * Example:
1280 1772 *
@@ -1691,9 +2183,9 @@
1691 2183 /**
1692 2184 * Returns the global styles custom CSS for a single block.
1693 2185 * This function is deprecated; please do not sync to core.
1694 2186 *
1695 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1696 2188 * @param string $selector The block selector.
1697 2189 *
1698 2190 * @return string The global styles custom CSS for the block.
1699 2191 */
@@ -1818,9 +2310,10 @@
1818 2310
1819 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1820 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1821 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1822 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1823 2316 // Use a fallback gap value if block gap support is not available.
1824 2317 if ( ! $has_block_gap_support ) {
1825 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1826 2319 if ( ! empty( $block_type ) ) {
@@ -1828,18 +2321,29 @@
1828 2321 }
1829 2322 } else {
1830 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1831 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1832 2326
1833 2327 // Support split row / column values and concatenate to a shorthand value.
1834 2328 if ( is_array( $block_gap_value ) ) {
1835 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1836 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1837 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1838 - $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;
1839 2342 } else {
1840 - // Skip outputting gap value if not all sides are provided.
1841 - $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;
1842 2346 }
1843 2347 }
1844 2348
1845 2349 // If the block should have custom gap, add the gap styles.
@@ -1849,10 +2353,13 @@
1849 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1850 2354 continue;
1851 2355 }
1852 2356
1853 - $class_name = $layout_definition['className'] ?? false;
1854 - $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;
1855 2362
1856 2363 if (
1857 2364 ! empty( $class_name ) &&
1858 2365 ! empty( $spacing_rules )
@@ -1865,9 +2372,9 @@
1865 2372 ! empty( $spacing_rule['rules'] )
1866 2373 ) {
1867 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1868 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1869 - $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;
1870 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1871 2378 $declarations[] = array(
1872 2379 'name' => $css_property,
1873 2380 'value' => $current_css_value,
@@ -2006,9 +2513,9 @@
2006 2513 * .has-value-gradient-background {
2007 2514 * background: value;
2008 2515 * }
2009 2516 *
2010 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
2011 2518 * background: value;
2012 2519 * }
2013 2520 *
2014 2521 * @since 5.9.0
@@ -2209,10 +2716,18 @@
2209 2716 foreach ( $slugs as $slug ) {
2210 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2211 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2212 2719
2213 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2214 - $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;
2215 2730 $stylesheet .= static::to_ruleset(
2216 2731 $new_selector,
2217 2732 array(
2218 2733 array(
@@ -2249,16 +2764,14 @@
2249 2764 if ( ! $scope || ! $selector ) {
2250 2765 return $selector;
2251 2766 }
2252 2767
2253 - $scopes = explode( ',', $scope );
2254 - $selectors = explode( ',', $selector );
2768 + $scopes = static::split_selector_list( $scope );
2769 + $selectors = static::split_selector_list( $selector );
2255 2770
2256 2771 $selectors_scoped = array();
2257 2772 foreach ( $scopes as $outer ) {
2258 2773 foreach ( $selectors as $inner ) {
2259 - $outer = trim( $outer );
2260 - $inner = trim( $inner );
2261 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2262 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2263 2776 } elseif ( empty( $outer ) ) {
2264 2777 $selectors_scoped[] = $inner;
@@ -2564,15 +3077,15 @@
2564 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2565 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.
2566 3079 * @since 6.7.0 Allow ref resolution of background properties.
2567 3080 *
2568 - * @param array $styles Styles to process.
2569 - * @param array $settings Theme settings.
2570 - * @param array $properties Properties metadata.
2571 - * @param array $theme_json Theme JSON array.
2572 - * @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.
2573 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2574 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2575 3088 */
2576 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2577 3090 if ( empty( $styles ) ) {
2578 3091 return array();
@@ -2696,10 +3209,10 @@
2696 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2697 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2698 3211 * @since 6.7.0 Added support for background image refs
2699 3212 *
2700 - * @param array $styles Styles subtree.
2701 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2702 3215 * @param array $theme_json Theme JSON array.
2703 3216 * @return string|array Style property value.
2704 3217 */
2705 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3071,9 +3584,9 @@
3071 3584 * @since 6.1.0
3072 3585 *
3073 3586 * @param array $theme_json The theme.json converted to an array.
3074 3587 * @param array $selectors Optional list of selectors per block.
3075 - * @param array $options {
3588 + * @param array $options {
3076 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3077 3590 *
3078 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3079 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3086,10 +3599,11 @@
3086 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3087 3600 return $nodes;
3088 3601 }
3089 3602
3090 - $include_variations = $options['include_block_style_variations'] ?? false;
3091 - $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 );
3092 3606
3093 3607 // If only node paths are to be returned, skip selector assignment.
3094 3608 if ( ! $include_node_paths_only ) {
3095 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3133,8 +3647,9 @@
3133 3647
3134 3648 if ( $include_variations && isset( $node['variations'] ) ) {
3135 3649 foreach ( $node['variations'] as $variation => $node ) {
3136 3650 $variation_selectors[] = array(
3651 + 'name' => $variation,
3137 3652 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3138 3653 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3139 3654 );
3140 3655 }
@@ -3153,14 +3668,14 @@
3153 3668
3154 3669 // Responsive block nodes: emit one node per breakpoint that has styles.
3155 3670 // These are rendered immediately after the base block node so that
3156 3671 // the cascade order is: .block{} → @media{.block{}}
3157 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3672 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3158 3673 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3159 3674 $nodes[] = array(
3160 3675 'name' => $name,
3161 3676 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3162 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3677 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3163 3678 'selector' => $selector,
3164 3679 'selectors' => $feature_selectors,
3165 3680 'elements' => $selectors[ $name ]['elements'] ?? array(),
3166 3681 'variations' => $variation_selectors,
@@ -3173,9 +3688,9 @@
3173 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3174 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3175 3690 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3176 3691 $has_responsive_pseudo = false;
3177 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3692 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3178 3693 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3179 3694 $has_responsive_pseudo = true;
3180 3695 break;
3181 3696 }
@@ -3218,14 +3733,14 @@
3218 3733
3219 3734 // Responsive pseudo nodes: emit one node per breakpoint that has
3220 3735 // this pseudo state, immediately after the default pseudo node.
3221 3736 // Cascade order: .block:hover{} → @media{.block:hover{}}
3222 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3737 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3223 3738 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3224 3739 $nodes[] = array(
3225 3740 'name' => $name,
3226 3741 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3227 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3742 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3228 3743 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3229 3744 'selectors' => $pseudo_feature_selectors,
3230 3745 'elements' => $selectors[ $name ]['elements'] ?? array(),
3231 3746 'variations' => $variation_selectors,
@@ -3235,9 +3750,9 @@
3235 3750 }
3236 3751 }
3237 3752 }
3238 3753
3239 - // Handle custom states (e.g. '@current' for navigation).
3754 + // Handle custom states (e.g. '-current' for navigation).
3240 3755 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3241 3756 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3242 3757 if (
3243 3758 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
@@ -3276,33 +3791,56 @@
3276 3791 }
3277 3792 }
3278 3793 }
3279 3794 }
3280 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3281 - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
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 ) {
3282 3812 $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3283 3813 if ( $include_node_paths_only ) {
3284 - $nodes[] = array(
3285 - 'path' => $element_path,
3286 - );
3814 + if ( isset( $block_node['elements'][ $element ] ) ) {
3815 + $nodes[] = array(
3816 + 'path' => $element_path,
3817 + );
3818 + }
3287 3819 continue;
3288 3820 }
3289 3821
3822 + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3823 + continue;
3824 + }
3825 +
3290 3826 $element_selector = $selectors[ $name ]['elements'][ $element ];
3291 3827
3292 - $nodes[] = array(
3293 - 'path' => $element_path,
3294 - 'selector' => $element_selector,
3295 - );
3828 + if ( isset( $block_node['elements'][ $element ] ) ) {
3829 + $nodes[] = array(
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3832 + );
3833 + }
3296 3834
3297 3835 // Responsive element nodes: one node per breakpoint that has
3298 3836 // styles for this element. Cascade: a{} → @media{a{}}
3299 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3837 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3300 3838 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3301 3839 $nodes[] = array(
3302 3840 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3303 3841 'selector' => $element_selector,
3304 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3842 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3305 3843 );
3306 3844 }
3307 3845 }
3308 3846
@@ -3308,45 +3846,29 @@
3308 3846
3309 3847 // Handle any pseudo selectors for the element.
3310 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3311 3849 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3312 - // Create element pseudo node if default or any responsive breakpoint has the pseudo.
3313 - $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] );
3314 - if ( ! $has_element_pseudo ) {
3315 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) {
3316 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3317 - $has_element_pseudo = true;
3318 - break;
3319 - }
3320 - }
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.
3853 + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3854 + $nodes[] = array(
3855 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
3856 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3857 + );
3321 3858 }
3322 3859
3323 - if ( $has_element_pseudo ) {
3324 - $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element );
3325 - if ( $include_node_paths_only ) {
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 ] ) ) {
3326 3865 $nodes[] = array(
3327 - 'path' => $element_pseudo_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 ],
3328 3869 );
3329 - continue;
3330 3870 }
3331 -
3332 - $nodes[] = array(
3333 - 'path' => $element_pseudo_path,
3334 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3335 - );
3336 -
3337 - // Responsive element pseudo nodes: one node per breakpoint
3338 - // that has this pseudo state for this element.
3339 - // Cascade: a:hover{} → @media{a:hover{}}
3340 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3341 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3342 - $nodes[] = array(
3343 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3344 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3345 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3346 - );
3347 - }
3348 - }
3349 3871 }
3350 3872 }
3351 3873 }
3352 3874 }
@@ -3366,14 +3888,15 @@
3366 3888 *
3367 3889 * @return string Styles for the block.
3368 3890 */
3369 3891 public function get_styles_for_block( $block_metadata ) {
3370 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3371 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3372 - $selector = $block_metadata['selector'];
3373 - $settings = $this->theme_json['settings'] ?? null;
3374 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3375 - $media_query = $block_metadata['media_query'] ?? null;
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 );
3376 3899
3377 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3378 3901
3379 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3386,16 +3909,16 @@
3386 3909
3387 3910 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3388 3911 // Responsive nodes (those with a media_query) do not process variations — variation responsive
3389 3912 // CSS is handled by the variation's own responsive nodes or the existing variation loop.
3390 - $style_variation_declarations = array();
3391 - $style_variation_custom_css = array();
3392 - $style_variation_responsive_css = array();
3393 - $style_variation_layout_metadata = array();
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();
3394 3918 if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) {
3395 3919 foreach ( $block_metadata['variations'] as $style_variation ) {
3396 - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3397 - $clean_style_variation_selector = trim( $style_variation['selector'] );
3920 + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3398 3921
3399 3922 // Generate any feature/subfeature style declarations for the current style variation.
3400 3923 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3401 3924
@@ -3406,27 +3929,10 @@
3406 3929 $variation_declarations = static::update_button_width_declarations( $variation_declarations, $settings );
3407 3930
3408 3931 // Combine selectors with style variation's selector and add to overall style variation declarations.
3409 3932 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3410 - /*
3411 - * Clean up any whitespace between comma separated selectors.
3412 - * This prevents these spaces breaking compound selectors such as:
3413 - * - `.wp-block-list:not(.wp-block-list .wp-block-list)`
3414 - * - `.wp-block-image img, .wp-block-image.my-class img`
3415 - */
3416 - $clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
3417 - $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector );
3933 + $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $current_selector );
3418 3934
3419 - // Prepend the variation selector to the current selector.
3420 - $split_selectors = explode( ',', $shortened_selector );
3421 - $updated_selectors = array_map(
3422 - static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3423 - return $clean_style_variation_selector . $split_selector;
3424 - },
3425 - $split_selectors
3426 - );
3427 - $combined_selectors = implode( ',', $updated_selectors );
3428 -
3429 3935 // Add the new declarations to the overall results under the modified selector.
3430 3936 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3431 3937 }
3432 3938 // Compute declarations for remaining styles not covered by feature level selectors.
@@ -3433,9 +3939,9 @@
3433 3939 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3434 3940
3435 3941 // Process pseudo-selectors for this variation (e.g., :hover, :focus).
3436 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 );
3437 - $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 );
3438 3944 $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3439 3945
3440 3946 // Store custom CSS for the style variation.
3441 3947 if ( isset( $style_variation_node['css'] ) ) {
@@ -3445,10 +3951,20 @@
3445 3951 // Store variation metadata and node for layout styles generation.
3446 3952 // Only store if the variation has blockGap defined.
3447 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3448 3954 // Append block selector to the variation selector for proper targeting.
3449 - $variation_metadata_with_selector = $style_variation;
3450 - $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 +
3451 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3452 3968 'metadata' => $variation_metadata_with_selector,
3453 3969 'node' => $style_variation_node,
3454 3970 );
@@ -3455,44 +3971,25 @@
3455 3971 }
3456 3972
3457 3973 // Store responsive breakpoint CSS for the style variation.
3458 3974 // This includes both base properties and feature-level selectors.
3459 - $variation_responsive_css = '';
3975 + $variation_responsive_css = '';
3976 + $variation_responsive_pseudo_css = '';
3460 3977
3461 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3978 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3462 3979 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3463 3980 continue;
3464 3981 }
3465 3982
3466 3983 $breakpoint_node = $style_variation_node[ $breakpoint ];
3467 - $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ];
3984 + $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3468 3985 // Process feature-level declarations for this breakpoint.
3469 3986 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3470 3987 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3471 3988 $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
3472 3989 foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) {
3473 - $clean_feature_selector = preg_replace( '/,\s+/', ',', $feature_selector );
3474 - $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_feature_selector );
3990 + $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $feature_selector );
3475 3991
3476 - if ( $block_metadata['selector'] && ! str_contains( $clean_feature_selector, $block_metadata['selector'] ) ) {
3477 - /*
3478 - * Feature selector is block-level (e.g. `.wp-block-button` for
3479 - * dimensions/width) — apply the variation class directly to it.
3480 - */
3481 - $feature_element_selector = str_replace( $shortened_selector, '', $clean_style_variation_selector );
3482 - $combined_selectors = str_replace( $feature_element_selector, '', $clean_style_variation_selector );
3483 - } else {
3484 - // Prepend the variation selector to the current selector.
3485 - $split_selectors = explode( ',', $shortened_selector );
3486 - $updated_selectors = array_map(
3487 - static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3488 - return $clean_style_variation_selector . $split_selector;
3489 - },
3490 - $split_selectors
3491 - );
3492 - $combined_selectors = implode( ',', $updated_selectors );
3493 - }
3494 -
3495 3992 $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl );
3496 3993 $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}';
3497 3994 }
3498 3995
@@ -3502,15 +3999,15 @@
3502 3999 $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
3503 4000 $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
3504 4001 }
3505 4002
3506 - $breakpoint_pseudo_declarations = static::process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name );
4003 + $breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3507 4004 foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) {
3508 4005 if ( empty( $pseudo_declarations ) ) {
3509 4006 continue;
3510 4007 }
3511 - $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations );
3512 - $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}';
4008 + $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations );
4009 + $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}';
3513 4010 }
3514 4011
3515 4012 // Process custom CSS for this breakpoint.
3516 4013 if ( isset( $breakpoint_node['css'] ) ) {
@@ -3521,9 +4018,13 @@
3521 4018 // Process blockGap responsive layout styles for this variation.
3522 4019 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
3523 4020 $variation_layout_metadata = $style_variation;
3524 4021 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
3525 - $variation_responsive_css .= $this->get_layout_styles(
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(
3526 4027 $variation_layout_metadata,
3527 4028 array(
3528 4029 'node' => $breakpoint_node,
3529 4030 'media_query' => $breakpoint_media,
@@ -3537,18 +4038,9 @@
3537 4038 if ( ! isset( $block_elements[ $element_name ] ) ) {
3538 4039 continue;
3539 4040 }
3540 4041
3541 - $clean_element_selector = preg_replace( '/,\s+/', ',', $block_elements[ $element_name ] );
3542 - $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_element_selector );
3543 - $split_selectors = explode( ',', $shortened_selector );
3544 - $updated_selectors = array_map(
3545 - static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3546 - return $clean_style_variation_selector . $split_selector;
3547 - },
3548 - $split_selectors
3549 - );
3550 - $variation_element_selector = implode( ',', $updated_selectors );
4042 + $variation_element_selector = static::get_block_style_variation_feature_selector( $style_variation, $block_elements[ $element_name ] );
3551 4043
3552 4044 $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json );
3553 4045 if ( ! empty( $element_declarations ) ) {
3554 4046 $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations );
@@ -3570,10 +4062,10 @@
3570 4062 if ( empty( $pseudo_declarations ) ) {
3571 4063 continue;
3572 4064 }
3573 4065
3574 - $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
3575 - $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
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 . '}';
3576 4068 }
3577 4069 }
3578 4070 }
3579 4071 }
@@ -3581,8 +4073,11 @@
3581 4073
3582 4074 if ( ! empty( $variation_responsive_css ) ) {
3583 4075 $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css;
3584 4076 }
4077 + if ( ! empty( $variation_responsive_pseudo_css ) ) {
4078 + $style_variation_responsive_pseudo_css[ $style_variation['selector'] ] = $variation_responsive_pseudo_css;
4079 + }
3585 4080 }
3586 4081 }
3587 4082 /*
3588 4083 * Get a reference to element name from path.
@@ -3750,8 +4245,15 @@
3750 4245 if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) {
3751 4246 $block_rules .= $style_variation_responsive_css[ $style_variation_selector ];
3752 4247 }
3753 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 + }
3754 4256
3755 4257 // Compute selector for block custom CSS.
3756 4258 $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
3757 4259 if ( is_array( $css_feature_selector ) ) {
@@ -4185,9 +4687,9 @@
4185 4687 * Gets a `default`'s preset name by a provided slug.
4186 4688 *
4187 4689 * @since 5.9.0
4188 4690 *
4189 - * @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.
4190 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
4191 4693 * @return string|null
4192 4694 */
4193 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -4235,10 +4737,10 @@
4235 4737 * @since 5.9.0
4236 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
4237 4739 *
4238 4740 * @param array $theme_json Structure to sanitize.
4239 - * @param string $origin Optional. What source of data this object represents.
4240 - * 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'.
4241 4743 * @return array Sanitized structure.
4242 4744 */
4243 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4244 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -4247,8 +4749,11 @@
4247 4749
4248 4750 $sanitized = array();
4249 4751
4250 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 + }
4251 4756
4252 4757 $blocks_metadata = static::get_blocks_metadata();
4253 4758 $valid_block_names = array_keys( $blocks_metadata );
4254 4759 $valid_element_names = array_keys( static::ELEMENTS );
@@ -4255,11 +4760,12 @@
4255 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
4256 4761
4257 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4258 4763
4259 - $blocks_metadata = static::get_blocks_metadata();
4260 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4261 - $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 );
4262 4768
4263 4769 foreach ( $style_nodes as $metadata ) {
4264 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4265 4771 if ( empty( $input ) ) {
@@ -4295,18 +4801,18 @@
4295 4801 }
4296 4802 }
4297 4803
4298 4804 // Re-add and process responsive breakpoint styles.
4299 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4805 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4300 4806 if ( isset( $input[ $breakpoint ] ) ) {
4301 4807 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4302 4808
4303 4809 if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4304 - $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] );
4810 + $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4305 4811 }
4306 4812
4307 4813 if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4308 - $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] );
4814 + $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4309 4815 }
4310 4816
4311 4817 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4312 4818 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4336,26 +4842,26 @@
4336 4842
4337 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4338 4844
4339 4845 if ( isset( $variation_input['blocks'] ) ) {
4340 - $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 );
4341 4847 }
4342 4848
4343 4849 if ( isset( $variation_input['elements'] ) ) {
4344 - $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 );
4345 4851 }
4346 4852
4347 4853 // Re-add and process responsive breakpoint styles for variations.
4348 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4854 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4349 4855 if ( isset( $variation_input[ $breakpoint ] ) ) {
4350 4856 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4351 4857
4352 4858 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4353 - $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] );
4859 + $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4354 4860 }
4355 4861
4356 4862 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4357 - $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] );
4863 + $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4358 4864 }
4359 4865
4360 4866 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4361 4867 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4385,9 +4891,9 @@
4385 4891 if ( empty( $input ) ) {
4386 4892 continue;
4387 4893 }
4388 4894
4389 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4390 4896 if ( ! empty( $output ) ) {
4391 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4392 4898 }
4393 4899 }
@@ -4409,14 +4915,19 @@
4409 4915
4410 4916 /**
4411 4917 * Remove insecure element styles within a variation or block.
4412 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 + *
4413 4922 * @since 6.8.0
4414 4923 *
4415 - * @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.
4416 4927 * @return array The sanitized elements styles.
4417 4928 */
4418 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4419 4930 $sanitized = array();
4420 4931 $valid_element_names = array_keys( static::ELEMENTS );
4421 4932
4422 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4431,17 +4942,19 @@
4431 4942 }
4432 4943 }
4433 4944 }
4434 4945
4435 - // Re-add and process responsive breakpoint styles for elements.
4436 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4437 - if ( isset( $element_input[ $breakpoint ] ) ) {
4438 - $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
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 ] );
4439 4951
4440 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4441 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4442 - if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4443 - $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
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 + }
4444 4957 }
4445 4958 }
4446 4959 }
4447 4960 }
@@ -4455,31 +4968,38 @@
4455 4968
4456 4969 /**
4457 4970 * Remove insecure styles from inner blocks and their elements.
4458 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 + *
4459 4975 * @since 6.8.0
4460 4976 *
4461 - * @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.
4462 4980 * @return array Sanitized block type styles.
4463 4981 */
4464 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4465 4983 $sanitized = array();
4466 4984 foreach ( $blocks as $block_type => $block_input ) {
4467 4985 $block_output = static::remove_insecure_styles( $block_input );
4468 4986
4469 4987 if ( isset( $block_input['elements'] ) ) {
4470 - $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 );
4471 4989 }
4472 4990
4473 - // Re-add and process responsive breakpoint styles for inner blocks.
4474 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4475 - if ( isset( $block_input[ $breakpoint ] ) ) {
4476 - $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
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 ] );
4477 4996
4478 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4479 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4480 - if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4481 - $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
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 + }
4482 5002 }
4483 5003 }
4484 5004 }
4485 5005 }
@@ -4524,12 +5044,14 @@
4524 5044 * without the insecure settings.
4525 5045 *
4526 5046 * @since 5.9.0
4527 5047 *
4528 - * @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.
4529 5051 * @return array
4530 5052 */
4531 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4532 5054 $output = array();
4533 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4534 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4535 5057 $path_with_origin = $preset_metadata['path'];
@@ -4580,8 +5102,12 @@
4580 5102
4581 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4582 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4583 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4584 5110 return $output;
4585 5111 }
4586 5112
4587 5113 /**
@@ -5258,8 +5784,9 @@
5258 5784 * This is used to convert the internal representation of variables to the CSS representation.
5259 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
5260 5786 *
5261 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
5262 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
5263 5790 * @return string The converted variable.
5264 5791 */
5265 5792 private static function convert_custom_properties( $value ) {
@@ -5266,15 +5793,32 @@
5266 5793 $prefix = 'var:';
5267 5794 $prefix_len = strlen( $prefix );
5268 5795 $token_in = '|';
5269 5796 $token_out = '--';
5270 - if ( 0 === strpos( $value, $prefix ) ) {
5271 - $unwrapped_name = str_replace(
5272 - $token_in,
5273 - $token_out,
5274 - substr( $value, $prefix_len )
5275 - );
5276 - $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 ) . ')';
5277 5821 }
5278 5822
5279 5823 return $value;
5280 5824 }
@@ -5283,9 +5827,9 @@
5283 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
5284 5828 * It is recursive and modifies the input in-place.
5285 5829 *
5286 5830 * @since 6.3.0
5287 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
5288 5832 * @return array The modified $tree.
5289 5833 */
5290 5834 private static function resolve_custom_css_format( $tree ) {
5291 5835 $prefix = 'var:';
@@ -5290,9 +5834,9 @@
5290 5834 private static function resolve_custom_css_format( $tree ) {
5291 5835 $prefix = 'var:';
5292 5836
5293 5837 foreach ( $tree as $key => $data ) {
5294 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
5295 5839 $tree[ $key ] = self::convert_custom_properties( $data );
5296 5840 } elseif ( is_array( $data ) ) {
5297 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
5298 5842 }
@@ -5395,16 +5939,27 @@
5395 5939 return $variation_class;
5396 5940 }
5397 5941
5398 5942 $limit = 1;
5399 - $selector_parts = explode( ',', $block_selector );
5943 + $selector_parts = static::split_selector_list( $block_selector );
5400 5944 $result = array();
5401 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 + */
5402 5957 foreach ( $selector_parts as $part ) {
5403 5958 $result[] = preg_replace_callback(
5404 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5405 5960 function ( $matches ) use ( $variation_class ) {
5406 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5407 5962 },
5408 5963 $part,
5409 5964 $limit
5410 5965 );
@@ -5409,9 +5964,52 @@
5409 5964 $limit
5410 5965 );
5411 5966 }
5412 5967
5413 - 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 + );
5414 6012 }
5415 6013
5416 6014 /**
5417 6015 * Collects valid block style variations keyed by block type.