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 +728 -210 23.3.0 → 24.1.0 View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -305,8 +315,9 @@
305 315 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 316 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 317 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 318 'text-decoration' => array( 'typography', 'textDecoration' ),
319 + 'text-shadow' => array( 'typography', 'textShadow' ),
309 320 'text-transform' => array( 'typography', 'textTransform' ),
310 321 'text-indent' => array( 'typography', 'textIndent' ),
311 322 'filter' => array( 'filter', 'duotone' ),
312 323 'box-shadow' => array( 'shadow' ),
@@ -388,10 +399,13 @@
388 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
389 400 * @since 6.4.0 Added `layout.allowEditing`.
390 401 * @since 6.4.0 Added `lightbox`.
391 402 * @since 7.0.0 Added type markers to the schema for boolean values.
392 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
393 404 * `typography.textIndent` properties.
405 + * @since 7.1.0 Added `viewport` property.
406 + * @since 7.2.0 Added `typography.textShadow`, `typography.textShadowPresets`,
407 + * and `typography.defaultTextShadowPresets`.
394 408 * @var array
395 409 */
396 410 const VALID_SETTINGS = array(
397 411 'appearanceTools' => null,
@@ -400,8 +414,11 @@
400 414 'backgroundImage' => null,
401 415 'backgroundSize' => null,
402 416 'gradient' => null,
403 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
404 421 'border' => array(
405 422 'color' => null,
406 423 'radius' => null,
407 424 'style' => null,
@@ -464,25 +481,32 @@
464 481 'presets' => null,
465 482 'defaultPresets' => null,
466 483 ),
467 484 'typography' => array(
468 - 'fluid' => null,
469 - 'customFontSize' => null,
470 - 'defaultFontSizes' => null,
471 - 'dropCap' => null,
472 - 'fontFamilies' => null,
473 - 'fontSizes' => null,
474 - 'fontStyle' => null,
475 - 'fontWeight' => null,
476 - 'letterSpacing' => null,
477 - 'lineHeight' => null,
478 - 'textAlign' => null,
479 - 'textColumns' => null,
480 - 'textDecoration' => null,
481 - 'textIndent' => null,
482 - 'textTransform' => null,
483 - 'writingMode' => null,
485 + 'fluid' => null,
486 + 'customFontSize' => null,
487 + 'defaultFontSizes' => null,
488 + 'dropCap' => null,
489 + 'fontFamilies' => null,
490 + 'fontSizes' => null,
491 + 'fontStyle' => null,
492 + 'fontWeight' => null,
493 + 'letterSpacing' => null,
494 + 'lineHeight' => null,
495 + 'textAlign' => null,
496 + 'textColumns' => null,
497 + 'textDecoration' => null,
498 + 'textIndent' => null,
499 + 'textTransform' => null,
500 + 'textShadow' => null,
501 + 'defaultTextShadowPresets' => null,
502 + 'textShadowPresets' => null,
503 + 'writingMode' => null,
484 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
485 509 );
486 510
487 511 const FONT_FAMILY_SCHEMA = array(
488 512 array(
@@ -521,9 +545,9 @@
521 545 * updated `blockGap` to be allowed at any level.
522 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
523 547 * @since 6.6.0 Added `background` sub properties to top-level only.
524 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
525 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
526 550 * `typography.textIndent` properties.
527 551 * @var array
528 552 */
529 553 const VALID_STYLES = array(
@@ -582,8 +606,9 @@
582 606 'textAlign' => null,
583 607 'textColumns' => null,
584 608 'textDecoration' => null,
585 609 'textIndent' => null,
610 + 'textShadow' => null,
586 611 'textTransform' => null,
587 612 'writingMode' => null,
588 613 ),
589 614 'css' => null,
@@ -620,29 +645,187 @@
620 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
621 646 );
622 647
623 648 /**
624 - * Responsive breakpoint state keys and their corresponding CSS media queries.
625 - * These are available for all blocks and wrap their styles in the given media query.
626 - * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx.
649 + * Default viewport breakpoint sizes.
627 650 *
628 651 * @since 7.1.0
629 652 * @var array
630 653 */
631 - const RESPONSIVE_BREAKPOINTS = array(
632 - 'mobile' => '@media (width <= 480px)',
633 - 'tablet' => '@media (480px < width <= 782px)',
654 + const DEFAULT_VIEWPORT_BREAKPOINTS = array(
655 + 'mobile' => '480px',
656 + 'tablet' => '782px',
634 657 );
635 658
636 659 /**
660 + * Returns CSS media queries for responsive viewport style states.
661 + *
662 + * Breakpoint values are read from `settings.viewport`, sanitized, and
663 + * normalized before the media query strings are generated. By default, the
664 + * returned keys are the theme.json style-state names (`@mobile`, `@tablet`).
665 + * When `$options['include_desktop']` is truthy, `@desktop` is included.
666 + *
667 + * @since 7.1.0
668 + *
669 + * @param mixed $viewport_settings Viewport settings from theme.json.
670 + * @param array $options {
671 + * Optional. Options for generating media queries.
672 + *
673 + * @type bool $include_desktop Whether to include the desktop media query. Default false.
674 + * }
675 + * @return array Responsive media queries.
676 + */
677 + public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) {
678 + $breakpoints = static::sanitize_viewport_settings( $viewport_settings );
679 +
680 + $responsive_media_queries = array();
681 +
682 + if ( isset( $breakpoints['mobile'] ) ) {
683 + $responsive_media_queries['@mobile'] = "@media (width <= {$breakpoints['mobile']})";
684 + }
685 +
686 + if ( isset( $breakpoints['tablet'] ) ) {
687 + $responsive_media_queries['@tablet'] = isset( $breakpoints['mobile'] )
688 + ? sprintf(
689 + '@media (%s < width <= %s)',
690 + $breakpoints['mobile'],
691 + $breakpoints['tablet']
692 + )
693 + : "@media (width <= {$breakpoints['tablet']})";
694 + }
695 +
696 + if ( ! empty( $options['include_desktop'] ) ) {
697 + if ( isset( $breakpoints['tablet'] ) ) {
698 + $desktop_breakpoint = $breakpoints['tablet'];
699 + } else {
700 + $desktop_breakpoint = $breakpoints['mobile'];
701 + }
702 +
703 + $responsive_media_queries['@desktop'] =
704 + "@media (width > {$desktop_breakpoint})";
705 + }
706 +
707 + return $responsive_media_queries;
708 + }
709 +
710 + /**
711 + * Checks whether a viewport breakpoint value is a safe CSS length.
712 + *
713 + * Viewport breakpoints are limited to numeric `px`, `em`, and `rem` lengths.
714 + * CSS functions, percentages, and other units are rejected because breakpoint
715 + * values are interpolated into generated media queries.
716 + *
717 + * @since 7.1.0
718 + *
719 + * @param mixed $value Value to check.
720 + * @return bool Whether the value is valid.
721 + */
722 + private static function is_valid_viewport_breakpoint_size( $value ) {
723 + if ( ! is_string( $value ) ) {
724 + return false;
725 + }
726 +
727 + $value = trim( $value );
728 + if ( '' === $value ) {
729 + return false;
730 + }
731 +
732 + return 1 === preg_match( '/^(?:\d+|\d*\.\d+)(?:px|em|rem)$/', $value );
733 + }
734 +
735 + /**
736 + * Converts a valid viewport breakpoint size to pixels for ordering checks.
737 + *
738 + * Generated media queries keep the original units. This method only
739 + * normalizes values so `mobile` and `tablet` can be compared safely. `em`
740 + * and `rem` lengths use a 16px base for comparison.
741 + *
742 + * @since 7.1.0
743 + *
744 + * @param mixed $value Viewport breakpoint size.
745 + * @return float|null Viewport breakpoint size in pixels, or null when invalid.
746 + */
747 + private static function get_viewport_breakpoint_value_in_pixels( $value ) {
748 + if ( ! static::is_valid_viewport_breakpoint_size( $value ) ) {
749 + return null;
750 + }
751 +
752 + $value = trim( $value );
753 + $unit = substr( $value, -3 );
754 + if ( 'rem' === $unit ) {
755 + $number = (float) substr( $value, 0, -3 );
756 + } else {
757 + $unit = substr( $value, -2 );
758 + $number = (float) substr( $value, 0, -2 );
759 + }
760 +
761 + /*
762 + * Use the most common browser default font size as the base for em/rem
763 + * media query conversions. This pixel value is only used to compare
764 + * breakpoint order; generated media queries keep the original units.
765 + */
766 + return 'px' === $unit ? $number : $number * 16;
767 + }
768 +
769 + /**
770 + * Sanitizes and normalizes viewport breakpoint settings.
771 + *
772 + * Keeps only supported breakpoint keys, trims valid CSS lengths, and returns
773 + * the default breakpoints when no valid custom breakpoint is provided. When
774 + * only one breakpoint is valid, it remains keyed by its configured state and
775 + * uses a single max-width media query. When `tablet` is not larger than
776 + * `mobile`, it is removed.
777 + *
778 + * @since 7.1.0
779 + *
780 + * @param mixed $viewport_settings Viewport settings from theme.json.
781 + * @return array Sanitized viewport breakpoint settings.
782 + */
783 + private static function sanitize_viewport_settings( $viewport_settings ) {
784 + if ( ! is_array( $viewport_settings ) ) {
785 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
786 + }
787 +
788 + $breakpoints = array();
789 + foreach ( array_keys( static::DEFAULT_VIEWPORT_BREAKPOINTS ) as $breakpoint ) {
790 + $value = $viewport_settings[ $breakpoint ] ?? null;
791 + $px = static::get_viewport_breakpoint_value_in_pixels( $value );
792 + if ( null !== $px ) {
793 + $breakpoints[ $breakpoint ] = array(
794 + 'value' => trim( $value ),
795 + 'px' => $px,
796 + );
797 + }
798 + }
799 +
800 + if ( empty( $breakpoints ) ) {
801 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
802 + }
803 +
804 + if ( 1 === count( $breakpoints ) ) {
805 + $breakpoint = key( $breakpoints );
806 + return array( $breakpoint => $breakpoints[ $breakpoint ]['value'] );
807 + }
808 +
809 + $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] );
810 +
811 + if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px']
812 + ) {
813 + $sanitized['tablet'] = $breakpoints['tablet']['value'];
814 + }
815 +
816 + return $sanitized;
817 + }
818 +
819 + /**
637 820 * Custom states for blocks that map to CSS class selectors rather than
638 - * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
639 - * distinguish them from real CSS pseudo-selectors.
821 + * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
822 + * distinguish them from real CSS pseudo-selectors and breakpoint states.
640 823 *
641 824 * The CSS selector for each state is defined in the block's block.json
642 825 * under `selectors.states`, e.g.:
643 826 *
644 - * "selectors": { "states": { "@current": ".some-css-selector" } }
827 + * "selectors": { "states": { "-current": ".some-css-selector" } }
645 828 *
646 829 * This constant controls which states are valid in theme.json for a given
647 830 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
648 831 * as valid sub-states, producing compound selectors such as
@@ -650,9 +833,9 @@
650 833 *
651 834 * @var array
652 835 */
653 836 const VALID_BLOCK_CUSTOM_STATES = array(
654 - 'core/navigation-link' => array( '@current' ),
837 + 'core/navigation-link' => array( '-current' ),
655 838 );
656 839
657 840 /**
658 841 * The valid elements that can be found under styles.
@@ -674,8 +857,9 @@
674 857 'button' => '.wp-element-button, .wp-block-button__link',
675 858 // The block classes are necessary to target older content that won't use the new class names.
676 859 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
677 860 'cite' => 'cite',
861 + 'label' => 'label',
678 862 'select' => 'select',
679 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
680 864 );
681 865
@@ -747,13 +931,15 @@
747 931
748 932 /**
749 933 * Processes pseudo-selectors for any node (block or variation).
750 934 *
751 - * @param array $node The node data (block or variation).
752 - * @param string $base_selector The base selector.
753 - * @param array $settings The theme settings.
754 - * @param string $block_name The block name.
755 - * @param array|null $block_metadata Metadata about the block to get styles for.
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.
756 942 * @param array|null $style_variation Style variation metadata.
757 943 * @return array Array of pseudo-selector declarations.
758 944 */
759 945 private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
@@ -887,9 +1073,12 @@
887 1073 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
888 1074 $origin = 'theme';
889 1075 }
890 1076
891 - $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 + }
892 1081 $blocks_metadata = static::get_blocks_metadata();
893 1082 $valid_block_names = array_keys( $blocks_metadata );
894 1083 $valid_element_names = array_keys( static::ELEMENTS );
895 1084 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
@@ -1058,8 +1247,10 @@
1058 1247 *
1059 1248 * @since 5.8.0
1060 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1061 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.
1062 1253 *
1063 1254 * @param array $input Structure to sanitize.
1064 1255 * @param array $valid_block_names List of valid block names.
1065 1256 * @param array $valid_element_names List of valid element names.
@@ -1094,10 +1285,11 @@
1094 1285 }
1095 1286 }
1096 1287
1097 1288 // Build the schema based on valid block & element names.
1098 - $schema = array();
1099 - $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 );
1100 1292
1101 1293 /*
1102 1294 * Set allowed element pseudo selectors and responsive breakpoint states.
1103 1295 * Target data structure in schema:
@@ -1103,9 +1295,9 @@
1103 1295 * Target data structure in schema:
1104 1296 * e.g.
1105 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1106 1298 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1107 - * - block responsive elements: `$schema['styles']['blocks']['core/button']['tablet']['elements']['link'][':hover']`.
1299 + * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1108 1300 */
1109 1301 foreach ( $valid_element_names as $element ) {
1110 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1111 1303
@@ -1115,9 +1307,9 @@
1115 1307 }
1116 1308 }
1117 1309
1118 1310 // Add responsive breakpoint states for elements.
1119 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1311 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1120 1312 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1121 1313 }
1122 1314 }
1123 1315
@@ -1122,9 +1314,17 @@
1122 1314 }
1123 1315
1124 1316 $schema_styles_blocks = array();
1125 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1126 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 +
1127 1327 /*
1128 1328 * Generate a schema for blocks.
1129 1329 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1130 1330 * - Variations definitions cannot be nested.
@@ -1132,20 +1332,29 @@
1132 1332 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1133 1333 *
1134 1334 * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1135 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.
1136 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 +
1137 1350 foreach ( $valid_block_names as $block ) {
1138 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1139 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1140 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1141 1353
1142 - // Add responsive breakpoint states for all blocks.
1143 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1144 - $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level;
1145 - $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1146 -
1147 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1354 + // Add responsive pseudo-selectors only to blocks that support them.
1355 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1356 + foreach ( $breakpoint_states as $breakpoint_state ) {
1148 1357 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1149 1358 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1150 1359 }
1151 1360 }
@@ -1157,9 +1366,9 @@
1157 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1158 1367 }
1159 1368 }
1160 1369
1161 - // 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).
1162 1371 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1163 1372 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1164 1373 $custom_state_schema = $styles_non_top_level;
1165 1374 // The same pseudo-selectors valid for the block at the top level
@@ -1197,12 +1406,11 @@
1197 1406 foreach ( $style_variation_names as $variation_name ) {
1198 1407 $variation_schema = $block_style_variation_styles;
1199 1408
1200 1409 // Add responsive breakpoint states to block style variations.
1201 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1410 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1202 1411 $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1203 1412 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1204 - $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks;
1205 1413
1206 1414 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1207 1415 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1208 1416 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1230,8 +1438,49 @@
1230 1438 $schema['settings'] = static::VALID_SETTINGS;
1231 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1232 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1233 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 +
1234 1483 // Remove anything that's not present in the schema.
1235 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1236 1485 if ( ! isset( $input[ $subtree ] ) ) {
1237 1486 continue;
@@ -1243,8 +1492,12 @@
1243 1492 }
1244 1493
1245 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1246 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1247 1500 if ( empty( $result ) ) {
1248 1501 unset( $output[ $subtree ] );
1249 1502 } else {
1250 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1272,14 +1525,38 @@
1272 1525 protected static function append_to_selector( $selector, $to_append ) {
1273 1526 if ( ! str_contains( $selector, ',' ) ) {
1274 1527 return $selector . $to_append;
1275 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 +
1276 1553 $new_selectors = array();
1277 1554 $selectors = static::split_selector_list( $selector );
1278 1555 foreach ( $selectors as $sel ) {
1279 1556 $new_selectors[] = $sel . $to_append;
1280 1557 }
1281 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1282 1559 }
1283 1560
1284 1561 /**
1285 1562 * Prepends a sub-selector to an existing one.
@@ -1297,49 +1574,194 @@
1297 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1298 1575 if ( ! str_contains( $selector, ',' ) ) {
1299 1576 return $to_prepend . $selector;
1300 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 +
1301 1602 $new_selectors = array();
1302 1603 $selectors = static::split_selector_list( $selector );
1303 1604 foreach ( $selectors as $sel ) {
1304 1605 $new_selectors[] = $to_prepend . $sel;
1305 1606 }
1306 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1307 1609 }
1308 1610
1309 1611 /**
1310 - * Splits a selector list by top-level commas.
1612 + * Splits a selector list into separate selectors.
1311 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 + *
1312 1645 * @param string $selector CSS selector list.
1313 1646 * @return string[] Selectors.
1314 1647 */
1315 - protected static function split_selector_list( $selector ) {
1648 + protected static function split_selector_list( $selector ): array {
1316 1649 if ( ! str_contains( $selector, ',' ) ) {
1317 - return array( $selector );
1650 + // See note on trimming CSS whitespace in main loop.
1651 + return array( trim( $selector, " \t\n" ) );
1318 1652 }
1319 1653
1320 1654 $selectors = array();
1321 - $current_selector = '';
1655 + $selector_length = strlen( $selector );
1322 1656 $parentheses_depth = 0;
1323 - $selector_length = strlen( $selector );
1657 + $at = 0;
1658 + $was_at = 0;
1324 1659
1325 - for ( $i = 0; $i < $selector_length; $i++ ) {
1326 - $char = $selector[ $i ];
1660 + while ( $at < $selector_length ) {
1661 + $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1662 + if ( $next_at >= $selector_length ) {
1663 + break;
1664 + }
1327 1665
1328 - if ( '(' === $char ) {
1329 - ++$parentheses_depth;
1330 - } elseif ( ')' === $char && $parentheses_depth > 0 ) {
1331 - --$parentheses_depth;
1332 - } elseif ( ',' === $char && 0 === $parentheses_depth ) {
1333 - $selectors[] = $current_selector;
1334 - $current_selector = '';
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 );
1335 1671 continue;
1336 1672 }
1337 1673
1338 - $current_selector .= $char;
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;
1339 1758 }
1340 1759
1341 - $selectors[] = $current_selector;
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 + }
1342 1764
1343 1765 return $selectors;
1344 1766 }
1345 1767
@@ -1761,9 +2183,9 @@
1761 2183 /**
1762 2184 * Returns the global styles custom CSS for a single block.
1763 2185 * This function is deprecated; please do not sync to core.
1764 2186 *
1765 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1766 2188 * @param string $selector The block selector.
1767 2189 *
1768 2190 * @return string The global styles custom CSS for the block.
1769 2191 */
@@ -1888,9 +2310,10 @@
1888 2310
1889 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1890 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1891 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1892 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1893 2316 // Use a fallback gap value if block gap support is not available.
1894 2317 if ( ! $has_block_gap_support ) {
1895 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1896 2319 if ( ! empty( $block_type ) ) {
@@ -1898,18 +2321,29 @@
1898 2321 }
1899 2322 } else {
1900 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1901 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1902 2326
1903 2327 // Support split row / column values and concatenate to a shorthand value.
1904 2328 if ( is_array( $block_gap_value ) ) {
1905 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1906 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1907 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1908 - $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;
1909 2342 } else {
1910 - // Skip outputting gap value if not all sides are provided.
1911 - $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;
1912 2346 }
1913 2347 }
1914 2348
1915 2349 // If the block should have custom gap, add the gap styles.
@@ -1919,10 +2353,13 @@
1919 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1920 2354 continue;
1921 2355 }
1922 2356
1923 - $class_name = $layout_definition['className'] ?? false;
1924 - $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;
1925 2362
1926 2363 if (
1927 2364 ! empty( $class_name ) &&
1928 2365 ! empty( $spacing_rules )
@@ -1935,9 +2372,9 @@
1935 2372 ! empty( $spacing_rule['rules'] )
1936 2373 ) {
1937 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1938 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1939 - $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;
1940 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1941 2378 $declarations[] = array(
1942 2379 'name' => $css_property,
1943 2380 'value' => $current_css_value,
@@ -2076,9 +2513,9 @@
2076 2513 * .has-value-gradient-background {
2077 2514 * background: value;
2078 2515 * }
2079 2516 *
2080 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
2081 2518 * background: value;
2082 2519 * }
2083 2520 *
2084 2521 * @since 5.9.0
@@ -2279,10 +2716,18 @@
2279 2716 foreach ( $slugs as $slug ) {
2280 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2281 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2282 2719
2283 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2284 - $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;
2285 2730 $stylesheet .= static::to_ruleset(
2286 2731 $new_selector,
2287 2732 array(
2288 2733 array(
@@ -2325,10 +2770,8 @@
2325 2770
2326 2771 $selectors_scoped = array();
2327 2772 foreach ( $scopes as $outer ) {
2328 2773 foreach ( $selectors as $inner ) {
2329 - $outer = trim( $outer );
2330 - $inner = trim( $inner );
2331 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2332 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2333 2776 } elseif ( empty( $outer ) ) {
2334 2777 $selectors_scoped[] = $inner;
@@ -2634,15 +3077,15 @@
2634 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2635 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.
2636 3079 * @since 6.7.0 Allow ref resolution of background properties.
2637 3080 *
2638 - * @param array $styles Styles to process.
2639 - * @param array $settings Theme settings.
2640 - * @param array $properties Properties metadata.
2641 - * @param array $theme_json Theme JSON array.
2642 - * @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.
2643 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2644 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2645 3088 */
2646 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2647 3090 if ( empty( $styles ) ) {
2648 3091 return array();
@@ -2766,10 +3209,10 @@
2766 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2767 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2768 3211 * @since 6.7.0 Added support for background image refs
2769 3212 *
2770 - * @param array $styles Styles subtree.
2771 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2772 3215 * @param array $theme_json Theme JSON array.
2773 3216 * @return string|array Style property value.
2774 3217 */
2775 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3141,9 +3584,9 @@
3141 3584 * @since 6.1.0
3142 3585 *
3143 3586 * @param array $theme_json The theme.json converted to an array.
3144 3587 * @param array $selectors Optional list of selectors per block.
3145 - * @param array $options {
3588 + * @param array $options {
3146 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3147 3590 *
3148 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3149 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3156,10 +3599,11 @@
3156 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3157 3600 return $nodes;
3158 3601 }
3159 3602
3160 - $include_variations = $options['include_block_style_variations'] ?? false;
3161 - $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 );
3162 3606
3163 3607 // If only node paths are to be returned, skip selector assignment.
3164 3608 if ( ! $include_node_paths_only ) {
3165 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3224,14 +3668,14 @@
3224 3668
3225 3669 // Responsive block nodes: emit one node per breakpoint that has styles.
3226 3670 // These are rendered immediately after the base block node so that
3227 3671 // the cascade order is: .block{} → @media{.block{}}
3228 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3672 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3229 3673 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3230 3674 $nodes[] = array(
3231 3675 'name' => $name,
3232 3676 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3233 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3677 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3234 3678 'selector' => $selector,
3235 3679 'selectors' => $feature_selectors,
3236 3680 'elements' => $selectors[ $name ]['elements'] ?? array(),
3237 3681 'variations' => $variation_selectors,
@@ -3244,9 +3688,9 @@
3244 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3245 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3246 3690 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3247 3691 $has_responsive_pseudo = false;
3248 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3692 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3249 3693 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3250 3694 $has_responsive_pseudo = true;
3251 3695 break;
3252 3696 }
@@ -3289,14 +3733,14 @@
3289 3733
3290 3734 // Responsive pseudo nodes: emit one node per breakpoint that has
3291 3735 // this pseudo state, immediately after the default pseudo node.
3292 3736 // Cascade order: .block:hover{} → @media{.block:hover{}}
3293 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3737 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3294 3738 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3295 3739 $nodes[] = array(
3296 3740 'name' => $name,
3297 3741 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3298 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3742 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3299 3743 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3300 3744 'selectors' => $pseudo_feature_selectors,
3301 3745 'elements' => $selectors[ $name ]['elements'] ?? array(),
3302 3746 'variations' => $variation_selectors,
@@ -3306,9 +3750,9 @@
3306 3750 }
3307 3751 }
3308 3752 }
3309 3753
3310 - // Handle custom states (e.g. '@current' for navigation).
3754 + // Handle custom states (e.g. '-current' for navigation).
3311 3755 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3312 3756 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3313 3757 if (
3314 3758 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
@@ -3347,33 +3791,56 @@
3347 3791 }
3348 3792 }
3349 3793 }
3350 3794 }
3351 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3352 - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3795 + /*
3796 + * Elements can be styled outside any breakpoint, inside one, or both,
3797 + * so collect the names from all of those places before looping. An
3798 + * element styled only inside a breakpoint still needs a node.
3799 + */
3800 + $block_node = $theme_json['styles']['blocks'][ $name ] ?? array();
3801 + $element_names = array_keys( $block_node['elements'] ?? array() );
3802 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3803 + $element_names = array_merge(
3804 + $element_names,
3805 + array_keys( $block_node[ $breakpoint ]['elements'] ?? array() )
3806 + );
3807 + }
3808 + $element_names = array_unique( $element_names );
3809 +
3810 + if ( ! empty( $element_names ) ) {
3811 + foreach ( $element_names as $element ) {
3353 3812 $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3354 3813 if ( $include_node_paths_only ) {
3355 - $nodes[] = array(
3356 - 'path' => $element_path,
3357 - );
3814 + if ( isset( $block_node['elements'][ $element ] ) ) {
3815 + $nodes[] = array(
3816 + 'path' => $element_path,
3817 + );
3818 + }
3358 3819 continue;
3359 3820 }
3360 3821
3822 + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3823 + continue;
3824 + }
3825 +
3361 3826 $element_selector = $selectors[ $name ]['elements'][ $element ];
3362 3827
3363 - $nodes[] = array(
3364 - 'path' => $element_path,
3365 - 'selector' => $element_selector,
3366 - );
3828 + if ( isset( $block_node['elements'][ $element ] ) ) {
3829 + $nodes[] = array(
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3832 + );
3833 + }
3367 3834
3368 3835 // Responsive element nodes: one node per breakpoint that has
3369 3836 // styles for this element. Cascade: a{} → @media{a{}}
3370 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3837 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3371 3838 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3372 3839 $nodes[] = array(
3373 3840 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3374 3841 'selector' => $element_selector,
3375 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3842 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3376 3843 );
3377 3844 }
3378 3845 }
3379 3846
@@ -3379,45 +3846,29 @@
3379 3846
3380 3847 // Handle any pseudo selectors for the element.
3381 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3382 3849 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3383 - // Create element pseudo node if default or any responsive breakpoint has the pseudo.
3384 - $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] );
3385 - if ( ! $has_element_pseudo ) {
3386 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) {
3387 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3388 - $has_element_pseudo = true;
3389 - break;
3390 - }
3391 - }
3850 + // Emit the default pseudo node only when the default state styles
3851 + // the pseudo. Otherwise get_styles_for_block() falls back to the
3852 + // element's base styles, outputting a rule the theme never defined.
3853 + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3854 + $nodes[] = array(
3855 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
3856 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3857 + );
3392 3858 }
3393 3859
3394 - if ( $has_element_pseudo ) {
3395 - $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element );
3396 - if ( $include_node_paths_only ) {
3860 + // Responsive element pseudo nodes: one node per breakpoint
3861 + // that has this pseudo state for this element.
3862 + // Cascade: a:hover{} → @media{a:hover{}}
3863 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3864 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3397 3865 $nodes[] = array(
3398 - 'path' => $element_pseudo_path,
3866 + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3867 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3868 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3399 3869 );
3400 - continue;
3401 3870 }
3402 -
3403 - $nodes[] = array(
3404 - 'path' => $element_pseudo_path,
3405 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3406 - );
3407 -
3408 - // Responsive element pseudo nodes: one node per breakpoint
3409 - // that has this pseudo state for this element.
3410 - // Cascade: a:hover{} → @media{a:hover{}}
3411 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3412 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3413 - $nodes[] = array(
3414 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3415 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3416 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3417 - );
3418 - }
3419 - }
3420 3871 }
3421 3872 }
3422 3873 }
3423 3874 }
@@ -3437,14 +3888,15 @@
3437 3888 *
3438 3889 * @return string Styles for the block.
3439 3890 */
3440 3891 public function get_styles_for_block( $block_metadata ) {
3441 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3442 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3443 - $selector = $block_metadata['selector'];
3444 - $settings = $this->theme_json['settings'] ?? null;
3445 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3446 - $media_query = $block_metadata['media_query'] ?? null;
3892 + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3893 + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3894 + $selector = $block_metadata['selector'];
3895 + $settings = $this->theme_json['settings'] ?? null;
3896 + $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3897 + $media_query = $block_metadata['media_query'] ?? null;
3898 + $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null );
3447 3899
3448 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3449 3901
3450 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3499,10 +3951,20 @@
3499 3951 // Store variation metadata and node for layout styles generation.
3500 3952 // Only store if the variation has blockGap defined.
3501 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3502 3954 // Append block selector to the variation selector for proper targeting.
3503 - $variation_metadata_with_selector = $style_variation;
3504 - $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 +
3505 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3506 3968 'metadata' => $variation_metadata_with_selector,
3507 3969 'node' => $style_variation_node,
3508 3970 );
@@ -3512,15 +3974,15 @@
3512 3974 // This includes both base properties and feature-level selectors.
3513 3975 $variation_responsive_css = '';
3514 3976 $variation_responsive_pseudo_css = '';
3515 3977
3516 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3978 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3517 3979 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3518 3980 continue;
3519 3981 }
3520 3982
3521 3983 $breakpoint_node = $style_variation_node[ $breakpoint ];
3522 - $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ];
3984 + $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3523 3985 // Process feature-level declarations for this breakpoint.
3524 3986 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3525 3987 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3526 3988 $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
@@ -3556,9 +4018,13 @@
3556 4018 // Process blockGap responsive layout styles for this variation.
3557 4019 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
3558 4020 $variation_layout_metadata = $style_variation;
3559 4021 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
3560 - $variation_responsive_css .= $this->get_layout_styles(
4022 +
4023 + // The variation slug is not a block name here either. See above.
4024 + $variation_layout_metadata['name'] = $block_name;
4025 +
4026 + $variation_responsive_css .= $this->get_layout_styles(
3561 4027 $variation_layout_metadata,
3562 4028 array(
3563 4029 'node' => $breakpoint_node,
3564 4030 'media_query' => $breakpoint_media,
@@ -4221,9 +4687,9 @@
4221 4687 * Gets a `default`'s preset name by a provided slug.
4222 4688 *
4223 4689 * @since 5.9.0
4224 4690 *
4225 - * @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.
4226 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
4227 4693 * @return string|null
4228 4694 */
4229 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -4271,10 +4737,10 @@
4271 4737 * @since 5.9.0
4272 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
4273 4739 *
4274 4740 * @param array $theme_json Structure to sanitize.
4275 - * @param string $origin Optional. What source of data this object represents.
4276 - * 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'.
4277 4743 * @return array Sanitized structure.
4278 4744 */
4279 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4280 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -4283,8 +4749,11 @@
4283 4749
4284 4750 $sanitized = array();
4285 4751
4286 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 + }
4287 4756
4288 4757 $blocks_metadata = static::get_blocks_metadata();
4289 4758 $valid_block_names = array_keys( $blocks_metadata );
4290 4759 $valid_element_names = array_keys( static::ELEMENTS );
@@ -4291,11 +4760,12 @@
4291 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
4292 4761
4293 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4294 4763
4295 - $blocks_metadata = static::get_blocks_metadata();
4296 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4297 - $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 );
4298 4768
4299 4769 foreach ( $style_nodes as $metadata ) {
4300 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4301 4771 if ( empty( $input ) ) {
@@ -4331,18 +4801,18 @@
4331 4801 }
4332 4802 }
4333 4803
4334 4804 // Re-add and process responsive breakpoint styles.
4335 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4805 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4336 4806 if ( isset( $input[ $breakpoint ] ) ) {
4337 4807 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4338 4808
4339 4809 if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4340 - $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] );
4810 + $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4341 4811 }
4342 4812
4343 4813 if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4344 - $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] );
4814 + $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4345 4815 }
4346 4816
4347 4817 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4348 4818 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4372,26 +4842,26 @@
4372 4842
4373 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4374 4844
4375 4845 if ( isset( $variation_input['blocks'] ) ) {
4376 - $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 );
4377 4847 }
4378 4848
4379 4849 if ( isset( $variation_input['elements'] ) ) {
4380 - $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 );
4381 4851 }
4382 4852
4383 4853 // Re-add and process responsive breakpoint styles for variations.
4384 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4854 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4385 4855 if ( isset( $variation_input[ $breakpoint ] ) ) {
4386 4856 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4387 4857
4388 4858 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4389 - $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] );
4859 + $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4390 4860 }
4391 4861
4392 4862 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4393 - $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] );
4863 + $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4394 4864 }
4395 4865
4396 4866 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4397 4867 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4421,9 +4891,9 @@
4421 4891 if ( empty( $input ) ) {
4422 4892 continue;
4423 4893 }
4424 4894
4425 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4426 4896 if ( ! empty( $output ) ) {
4427 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4428 4898 }
4429 4899 }
@@ -4445,14 +4915,19 @@
4445 4915
4446 4916 /**
4447 4917 * Remove insecure element styles within a variation or block.
4448 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 + *
4449 4922 * @since 6.8.0
4450 4923 *
4451 - * @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.
4452 4927 * @return array The sanitized elements styles.
4453 4928 */
4454 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4455 4930 $sanitized = array();
4456 4931 $valid_element_names = array_keys( static::ELEMENTS );
4457 4932
4458 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4467,17 +4942,19 @@
4467 4942 }
4468 4943 }
4469 4944 }
4470 4945
4471 - // Re-add and process responsive breakpoint styles for elements.
4472 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4473 - if ( isset( $element_input[ $breakpoint ] ) ) {
4474 - $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4946 + if ( null !== $responsive_media_queries ) {
4947 + // Re-add and process responsive breakpoint styles for elements.
4948 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4949 + if ( isset( $element_input[ $breakpoint ] ) ) {
4950 + $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4475 4951
4476 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4477 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4478 - if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4479 - $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4952 + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4953 + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4954 + if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4955 + $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4956 + }
4480 4957 }
4481 4958 }
4482 4959 }
4483 4960 }
@@ -4491,31 +4968,38 @@
4491 4968
4492 4969 /**
4493 4970 * Remove insecure styles from inner blocks and their elements.
4494 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 + *
4495 4975 * @since 6.8.0
4496 4976 *
4497 - * @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.
4498 4980 * @return array Sanitized block type styles.
4499 4981 */
4500 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4501 4983 $sanitized = array();
4502 4984 foreach ( $blocks as $block_type => $block_input ) {
4503 4985 $block_output = static::remove_insecure_styles( $block_input );
4504 4986
4505 4987 if ( isset( $block_input['elements'] ) ) {
4506 - $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 );
4507 4989 }
4508 4990
4509 - // Re-add and process responsive breakpoint styles for inner blocks.
4510 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4511 - if ( isset( $block_input[ $breakpoint ] ) ) {
4512 - $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4991 + if ( null !== $responsive_media_queries ) {
4992 + // Re-add and process responsive breakpoint styles for inner blocks.
4993 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4994 + if ( isset( $block_input[ $breakpoint ] ) ) {
4995 + $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4513 4996
4514 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4515 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4516 - if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4517 - $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
4997 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4998 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4999 + if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
5000 + $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
5001 + }
4518 5002 }
4519 5003 }
4520 5004 }
4521 5005 }
@@ -4560,12 +5044,14 @@
4560 5044 * without the insecure settings.
4561 5045 *
4562 5046 * @since 5.9.0
4563 5047 *
4564 - * @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.
4565 5051 * @return array
4566 5052 */
4567 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4568 5054 $output = array();
4569 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4570 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4571 5057 $path_with_origin = $preset_metadata['path'];
@@ -4616,8 +5102,12 @@
4616 5102
4617 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4618 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4619 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4620 5110 return $output;
4621 5111 }
4622 5112
4623 5113 /**
@@ -5294,8 +5784,9 @@
5294 5784 * This is used to convert the internal representation of variables to the CSS representation.
5295 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
5296 5786 *
5297 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
5298 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
5299 5790 * @return string The converted variable.
5300 5791 */
5301 5792 private static function convert_custom_properties( $value ) {
@@ -5302,15 +5793,32 @@
5302 5793 $prefix = 'var:';
5303 5794 $prefix_len = strlen( $prefix );
5304 5795 $token_in = '|';
5305 5796 $token_out = '--';
5306 - if ( 0 === strpos( $value, $prefix ) ) {
5307 - $unwrapped_name = str_replace(
5308 - $token_in,
5309 - $token_out,
5310 - substr( $value, $prefix_len )
5311 - );
5312 - $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 ) . ')';
5313 5821 }
5314 5822
5315 5823 return $value;
5316 5824 }
@@ -5319,9 +5827,9 @@
5319 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
5320 5828 * It is recursive and modifies the input in-place.
5321 5829 *
5322 5830 * @since 6.3.0
5323 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
5324 5832 * @return array The modified $tree.
5325 5833 */
5326 5834 private static function resolve_custom_css_format( $tree ) {
5327 5835 $prefix = 'var:';
@@ -5326,9 +5834,9 @@
5326 5834 private static function resolve_custom_css_format( $tree ) {
5327 5835 $prefix = 'var:';
5328 5836
5329 5837 foreach ( $tree as $key => $data ) {
5330 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
5331 5839 $tree[ $key ] = self::convert_custom_properties( $data );
5332 5840 } elseif ( is_array( $data ) ) {
5333 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
5334 5842 }
@@ -5434,13 +5942,24 @@
5434 5942 $limit = 1;
5435 5943 $selector_parts = static::split_selector_list( $block_selector );
5436 5944 $result = array();
5437 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 + */
5438 5957 foreach ( $selector_parts as $part ) {
5439 5958 $result[] = preg_replace_callback(
5440 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5441 5960 function ( $matches ) use ( $variation_class ) {
5442 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5443 5962 },
5444 5963 $part,
5445 5964 $limit
5446 5965 );
@@ -5445,9 +5964,9 @@
5445 5964 $limit
5446 5965 );
5447 5966 }
5448 5967
5449 - return implode( ',', $result );
5968 + return implode( ', ', $result );
5450 5969 }
5451 5970
5452 5971 /**
5453 5972 * Applies a block style variation class to a feature selector.
@@ -5458,9 +5977,9 @@
5458 5977 * the variation class directly to the selector that will receive the
5459 5978 * declarations instead of deriving it by subtracting the root selector from
5460 5979 * the feature selector.
5461 5980 *
5462 - * @param array $style_variation Style variation metadata.
5981 + * @param array $style_variation Style variation metadata.
5463 5982 * @param string $feature_selector CSS selector for the feature.
5464 5983 * @return string Feature selector with block style variation selector added.
5465 5984 */
5466 5985 protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
@@ -5474,10 +5993,9 @@
5474 5993 $variation_class = ".is-style-$variation_name";
5475 5994 $selector_parts = static::split_selector_list( $feature_selector );
5476 5995 $selector_parts = array_map(
5477 5996 static function ( $selector ) use ( $variation_class ) {
5478 - $selector = trim( $selector );
5479 - $prefix = $variation_class . ' ';
5997 + $prefix = $variation_class . ' ';
5480 5998
5481 5999 if ( str_starts_with( $selector, $prefix ) ) {
5482 6000 return substr( $selector, strlen( $prefix ) );
5483 6001 }
@@ -5488,9 +6006,9 @@
5488 6006 );
5489 6007
5490 6008 return static::get_block_style_variation_selector(
5491 6009 $variation_name,
5492 - implode( ',', $selector_parts )
6010 + implode( ', ', $selector_parts )
5493 6011 );
5494 6012 }
5495 6013
5496 6014 /**