PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/class-wp-theme-json-gutenberg.php +858 -260 23.2.1 → 24.1.0 View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -305,8 +315,9 @@
305 315 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 316 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 317 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 318 'text-decoration' => array( 'typography', 'textDecoration' ),
319 + 'text-shadow' => array( 'typography', 'textShadow' ),
309 320 'text-transform' => array( 'typography', 'textTransform' ),
310 321 'text-indent' => array( 'typography', 'textIndent' ),
311 322 'filter' => array( 'filter', 'duotone' ),
312 323 'box-shadow' => array( 'shadow' ),
@@ -388,10 +399,13 @@
388 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
389 400 * @since 6.4.0 Added `layout.allowEditing`.
390 401 * @since 6.4.0 Added `lightbox`.
391 402 * @since 7.0.0 Added type markers to the schema for boolean values.
392 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
393 404 * `typography.textIndent` properties.
405 + * @since 7.1.0 Added `viewport` property.
406 + * @since 7.2.0 Added `typography.textShadow`, `typography.textShadowPresets`,
407 + * and `typography.defaultTextShadowPresets`.
394 408 * @var array
395 409 */
396 410 const VALID_SETTINGS = array(
397 411 'appearanceTools' => null,
@@ -400,8 +414,11 @@
400 414 'backgroundImage' => null,
401 415 'backgroundSize' => null,
402 416 'gradient' => null,
403 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
404 421 'border' => array(
405 422 'color' => null,
406 423 'radius' => null,
407 424 'style' => null,
@@ -464,25 +481,32 @@
464 481 'presets' => null,
465 482 'defaultPresets' => null,
466 483 ),
467 484 'typography' => array(
468 - 'fluid' => null,
469 - 'customFontSize' => null,
470 - 'defaultFontSizes' => null,
471 - 'dropCap' => null,
472 - 'fontFamilies' => null,
473 - 'fontSizes' => null,
474 - 'fontStyle' => null,
475 - 'fontWeight' => null,
476 - 'letterSpacing' => null,
477 - 'lineHeight' => null,
478 - 'textAlign' => null,
479 - 'textColumns' => null,
480 - 'textDecoration' => null,
481 - 'textIndent' => null,
482 - 'textTransform' => null,
483 - 'writingMode' => null,
485 + 'fluid' => null,
486 + 'customFontSize' => null,
487 + 'defaultFontSizes' => null,
488 + 'dropCap' => null,
489 + 'fontFamilies' => null,
490 + 'fontSizes' => null,
491 + 'fontStyle' => null,
492 + 'fontWeight' => null,
493 + 'letterSpacing' => null,
494 + 'lineHeight' => null,
495 + 'textAlign' => null,
496 + 'textColumns' => null,
497 + 'textDecoration' => null,
498 + 'textIndent' => null,
499 + 'textTransform' => null,
500 + 'textShadow' => null,
501 + 'defaultTextShadowPresets' => null,
502 + 'textShadowPresets' => null,
503 + 'writingMode' => null,
484 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
485 509 );
486 510
487 511 const FONT_FAMILY_SCHEMA = array(
488 512 array(
@@ -521,9 +545,9 @@
521 545 * updated `blockGap` to be allowed at any level.
522 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
523 547 * @since 6.6.0 Added `background` sub properties to top-level only.
524 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
525 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
526 550 * `typography.textIndent` properties.
527 551 * @var array
528 552 */
529 553 const VALID_STYLES = array(
@@ -582,8 +606,9 @@
582 606 'textAlign' => null,
583 607 'textColumns' => null,
584 608 'textDecoration' => null,
585 609 'textIndent' => null,
610 + 'textShadow' => null,
586 611 'textTransform' => null,
587 612 'writingMode' => null,
588 613 ),
589 614 'css' => null,
@@ -620,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.