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