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 +713 -203 23.5.2 → 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 */
@@ -389,10 +399,13 @@
389 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
390 400 * @since 6.4.0 Added `layout.allowEditing`.
391 401 * @since 6.4.0 Added `lightbox`.
392 402 * @since 7.0.0 Added type markers to the schema for boolean values.
393 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
394 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`.
395 408 * @var array
396 409 */
397 410 const VALID_SETTINGS = array(
398 411 'appearanceTools' => null,
@@ -401,8 +414,11 @@
401 414 'backgroundImage' => null,
402 415 'backgroundSize' => null,
403 416 'gradient' => null,
404 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
405 421 'border' => array(
406 422 'color' => null,
407 423 'radius' => null,
408 424 'style' => null,
@@ -465,25 +481,32 @@
465 481 'presets' => null,
466 482 'defaultPresets' => null,
467 483 ),
468 484 'typography' => array(
469 - 'fluid' => null,
470 - 'customFontSize' => null,
471 - 'defaultFontSizes' => null,
472 - 'dropCap' => null,
473 - 'fontFamilies' => null,
474 - 'fontSizes' => null,
475 - 'fontStyle' => null,
476 - 'fontWeight' => null,
477 - 'letterSpacing' => null,
478 - 'lineHeight' => null,
479 - 'textAlign' => null,
480 - 'textColumns' => null,
481 - 'textDecoration' => null,
482 - 'textIndent' => null,
483 - 'textTransform' => null,
484 - '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,
485 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
486 509 );
487 510
488 511 const FONT_FAMILY_SCHEMA = array(
489 512 array(
@@ -522,9 +545,9 @@
522 545 * updated `blockGap` to be allowed at any level.
523 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
524 547 * @since 6.6.0 Added `background` sub properties to top-level only.
525 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
526 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
527 550 * `typography.textIndent` properties.
528 551 * @var array
529 552 */
530 553 const VALID_STYLES = array(
@@ -622,21 +645,179 @@
622 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
623 646 );
624 647
625 648 /**
626 - * Responsive breakpoint state keys and their corresponding CSS media queries.
627 - * These are available for all blocks and wrap their styles in the given media query.
628 - * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx.
649 + * Default viewport breakpoint sizes.
629 650 *
630 651 * @since 7.1.0
631 652 * @var array
632 653 */
633 - const RESPONSIVE_BREAKPOINTS = array(
634 - '@mobile' => '@media (width <= 480px)',
635 - '@tablet' => '@media (480px < width <= 782px)',
654 + const DEFAULT_VIEWPORT_BREAKPOINTS = array(
655 + 'mobile' => '480px',
656 + 'tablet' => '782px',
636 657 );
637 658
638 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 + /**
639 820 * Custom states for blocks that map to CSS class selectors rather than
640 821 * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
641 822 * distinguish them from real CSS pseudo-selectors and breakpoint states.
642 823 *
@@ -676,8 +857,9 @@
676 857 'button' => '.wp-element-button, .wp-block-button__link',
677 858 // The block classes are necessary to target older content that won't use the new class names.
678 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',
679 860 'cite' => 'cite',
861 + 'label' => 'label',
680 862 'select' => 'select',
681 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
682 864 );
683 865
@@ -749,13 +931,15 @@
749 931
750 932 /**
751 933 * Processes pseudo-selectors for any node (block or variation).
752 934 *
753 - * @param array $node The node data (block or variation).
754 - * @param string $base_selector The base selector.
755 - * @param array $settings The theme settings.
756 - * @param string $block_name The block name.
757 - * @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.
758 942 * @param array|null $style_variation Style variation metadata.
759 943 * @return array Array of pseudo-selector declarations.
760 944 */
761 945 private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
@@ -1063,8 +1247,10 @@
1063 1247 *
1064 1248 * @since 5.8.0
1065 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1066 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.
1067 1253 *
1068 1254 * @param array $input Structure to sanitize.
1069 1255 * @param array $valid_block_names List of valid block names.
1070 1256 * @param array $valid_element_names List of valid element names.
@@ -1099,10 +1285,11 @@
1099 1285 }
1100 1286 }
1101 1287
1102 1288 // Build the schema based on valid block & element names.
1103 - $schema = array();
1104 - $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 );
1105 1292
1106 1293 /*
1107 1294 * Set allowed element pseudo selectors and responsive breakpoint states.
1108 1295 * Target data structure in schema:
@@ -1108,9 +1295,9 @@
1108 1295 * Target data structure in schema:
1109 1296 * e.g.
1110 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1111 1298 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1112 - * - 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']`.
1113 1300 */
1114 1301 foreach ( $valid_element_names as $element ) {
1115 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1116 1303
@@ -1120,9 +1307,9 @@
1120 1307 }
1121 1308 }
1122 1309
1123 1310 // Add responsive breakpoint states for elements.
1124 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1311 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1125 1312 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1126 1313 }
1127 1314 }
1128 1315
@@ -1127,9 +1314,17 @@
1127 1314 }
1128 1315
1129 1316 $schema_styles_blocks = array();
1130 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1131 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 +
1132 1327 /*
1133 1328 * Generate a schema for blocks.
1134 1329 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1135 1330 * - Variations definitions cannot be nested.
@@ -1137,20 +1332,29 @@
1137 1332 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1138 1333 *
1139 1334 * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1140 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.
1141 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 +
1142 1350 foreach ( $valid_block_names as $block ) {
1143 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1144 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1145 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1146 1353
1147 - // Add responsive breakpoint states for all blocks.
1148 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1149 - $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level;
1150 - $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1151 -
1152 - 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 ) {
1153 1357 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1154 1358 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1155 1359 }
1156 1360 }
@@ -1202,12 +1406,11 @@
1202 1406 foreach ( $style_variation_names as $variation_name ) {
1203 1407 $variation_schema = $block_style_variation_styles;
1204 1408
1205 1409 // Add responsive breakpoint states to block style variations.
1206 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1410 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1207 1411 $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1208 1412 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1209 - $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks;
1210 1413
1211 1414 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1212 1415 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1213 1416 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1235,8 +1438,49 @@
1235 1438 $schema['settings'] = static::VALID_SETTINGS;
1236 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1237 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1238 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 +
1239 1483 // Remove anything that's not present in the schema.
1240 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1241 1485 if ( ! isset( $input[ $subtree ] ) ) {
1242 1486 continue;
@@ -1248,8 +1492,12 @@
1248 1492 }
1249 1493
1250 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1251 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1252 1500 if ( empty( $result ) ) {
1253 1501 unset( $output[ $subtree ] );
1254 1502 } else {
1255 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1277,14 +1525,38 @@
1277 1525 protected static function append_to_selector( $selector, $to_append ) {
1278 1526 if ( ! str_contains( $selector, ',' ) ) {
1279 1527 return $selector . $to_append;
1280 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 +
1281 1553 $new_selectors = array();
1282 1554 $selectors = static::split_selector_list( $selector );
1283 1555 foreach ( $selectors as $sel ) {
1284 1556 $new_selectors[] = $sel . $to_append;
1285 1557 }
1286 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1287 1559 }
1288 1560
1289 1561 /**
1290 1562 * Prepends a sub-selector to an existing one.
@@ -1302,49 +1574,194 @@
1302 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1303 1575 if ( ! str_contains( $selector, ',' ) ) {
1304 1576 return $to_prepend . $selector;
1305 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 +
1306 1602 $new_selectors = array();
1307 1603 $selectors = static::split_selector_list( $selector );
1308 1604 foreach ( $selectors as $sel ) {
1309 1605 $new_selectors[] = $to_prepend . $sel;
1310 1606 }
1311 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1312 1609 }
1313 1610
1314 1611 /**
1315 - * Splits a selector list by top-level commas.
1612 + * Splits a selector list into separate selectors.
1316 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 + *
1317 1645 * @param string $selector CSS selector list.
1318 1646 * @return string[] Selectors.
1319 1647 */
1320 - protected static function split_selector_list( $selector ) {
1648 + protected static function split_selector_list( $selector ): array {
1321 1649 if ( ! str_contains( $selector, ',' ) ) {
1322 - return array( $selector );
1650 + // See note on trimming CSS whitespace in main loop.
1651 + return array( trim( $selector, " \t\n" ) );
1323 1652 }
1324 1653
1325 1654 $selectors = array();
1326 - $current_selector = '';
1655 + $selector_length = strlen( $selector );
1327 1656 $parentheses_depth = 0;
1328 - $selector_length = strlen( $selector );
1657 + $at = 0;
1658 + $was_at = 0;
1329 1659
1330 - for ( $i = 0; $i < $selector_length; $i++ ) {
1331 - $char = $selector[ $i ];
1660 + while ( $at < $selector_length ) {
1661 + $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1662 + if ( $next_at >= $selector_length ) {
1663 + break;
1664 + }
1332 1665
1333 - if ( '(' === $char ) {
1334 - ++$parentheses_depth;
1335 - } elseif ( ')' === $char && $parentheses_depth > 0 ) {
1336 - --$parentheses_depth;
1337 - } elseif ( ',' === $char && 0 === $parentheses_depth ) {
1338 - $selectors[] = $current_selector;
1339 - $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 );
1340 1671 continue;
1341 1672 }
1342 1673
1343 - $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;
1344 1758 }
1345 1759
1346 - $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 + }
1347 1764
1348 1765 return $selectors;
1349 1766 }
1350 1767
@@ -1766,9 +2183,9 @@
1766 2183 /**
1767 2184 * Returns the global styles custom CSS for a single block.
1768 2185 * This function is deprecated; please do not sync to core.
1769 2186 *
1770 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1771 2188 * @param string $selector The block selector.
1772 2189 *
1773 2190 * @return string The global styles custom CSS for the block.
1774 2191 */
@@ -1893,9 +2310,10 @@
1893 2310
1894 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1895 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1896 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1897 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1898 2316 // Use a fallback gap value if block gap support is not available.
1899 2317 if ( ! $has_block_gap_support ) {
1900 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1901 2319 if ( ! empty( $block_type ) ) {
@@ -1903,18 +2321,29 @@
1903 2321 }
1904 2322 } else {
1905 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1906 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1907 2326
1908 2327 // Support split row / column values and concatenate to a shorthand value.
1909 2328 if ( is_array( $block_gap_value ) ) {
1910 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1911 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1912 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1913 - $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;
1914 2342 } else {
1915 - // Skip outputting gap value if not all sides are provided.
1916 - $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;
1917 2346 }
1918 2347 }
1919 2348
1920 2349 // If the block should have custom gap, add the gap styles.
@@ -1924,10 +2353,13 @@
1924 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1925 2354 continue;
1926 2355 }
1927 2356
1928 - $class_name = $layout_definition['className'] ?? false;
1929 - $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;
1930 2362
1931 2363 if (
1932 2364 ! empty( $class_name ) &&
1933 2365 ! empty( $spacing_rules )
@@ -1940,9 +2372,9 @@
1940 2372 ! empty( $spacing_rule['rules'] )
1941 2373 ) {
1942 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1943 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1944 - $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;
1945 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1946 2378 $declarations[] = array(
1947 2379 'name' => $css_property,
1948 2380 'value' => $current_css_value,
@@ -2081,9 +2513,9 @@
2081 2513 * .has-value-gradient-background {
2082 2514 * background: value;
2083 2515 * }
2084 2516 *
2085 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
2086 2518 * background: value;
2087 2519 * }
2088 2520 *
2089 2521 * @since 5.9.0
@@ -2284,10 +2716,18 @@
2284 2716 foreach ( $slugs as $slug ) {
2285 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2286 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2287 2719
2288 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2289 - $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;
2290 2730 $stylesheet .= static::to_ruleset(
2291 2731 $new_selector,
2292 2732 array(
2293 2733 array(
@@ -2330,10 +2770,8 @@
2330 2770
2331 2771 $selectors_scoped = array();
2332 2772 foreach ( $scopes as $outer ) {
2333 2773 foreach ( $selectors as $inner ) {
2334 - $outer = trim( $outer );
2335 - $inner = trim( $inner );
2336 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2337 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2338 2776 } elseif ( empty( $outer ) ) {
2339 2777 $selectors_scoped[] = $inner;
@@ -2639,15 +3077,15 @@
2639 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2640 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.
2641 3079 * @since 6.7.0 Allow ref resolution of background properties.
2642 3080 *
2643 - * @param array $styles Styles to process.
2644 - * @param array $settings Theme settings.
2645 - * @param array $properties Properties metadata.
2646 - * @param array $theme_json Theme JSON array.
2647 - * @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.
2648 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2649 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2650 3088 */
2651 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2652 3090 if ( empty( $styles ) ) {
2653 3091 return array();
@@ -2771,10 +3209,10 @@
2771 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2772 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2773 3211 * @since 6.7.0 Added support for background image refs
2774 3212 *
2775 - * @param array $styles Styles subtree.
2776 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2777 3215 * @param array $theme_json Theme JSON array.
2778 3216 * @return string|array Style property value.
2779 3217 */
2780 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3146,9 +3584,9 @@
3146 3584 * @since 6.1.0
3147 3585 *
3148 3586 * @param array $theme_json The theme.json converted to an array.
3149 3587 * @param array $selectors Optional list of selectors per block.
3150 - * @param array $options {
3588 + * @param array $options {
3151 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3152 3590 *
3153 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3154 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3161,10 +3599,11 @@
3161 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3162 3600 return $nodes;
3163 3601 }
3164 3602
3165 - $include_variations = $options['include_block_style_variations'] ?? false;
3166 - $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 );
3167 3606
3168 3607 // If only node paths are to be returned, skip selector assignment.
3169 3608 if ( ! $include_node_paths_only ) {
3170 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3229,14 +3668,14 @@
3229 3668
3230 3669 // Responsive block nodes: emit one node per breakpoint that has styles.
3231 3670 // These are rendered immediately after the base block node so that
3232 3671 // the cascade order is: .block{} → @media{.block{}}
3233 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3672 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3234 3673 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3235 3674 $nodes[] = array(
3236 3675 'name' => $name,
3237 3676 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3238 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3677 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3239 3678 'selector' => $selector,
3240 3679 'selectors' => $feature_selectors,
3241 3680 'elements' => $selectors[ $name ]['elements'] ?? array(),
3242 3681 'variations' => $variation_selectors,
@@ -3249,9 +3688,9 @@
3249 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3250 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3251 3690 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3252 3691 $has_responsive_pseudo = false;
3253 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3692 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3254 3693 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3255 3694 $has_responsive_pseudo = true;
3256 3695 break;
3257 3696 }
@@ -3294,14 +3733,14 @@
3294 3733
3295 3734 // Responsive pseudo nodes: emit one node per breakpoint that has
3296 3735 // this pseudo state, immediately after the default pseudo node.
3297 3736 // Cascade order: .block:hover{} → @media{.block:hover{}}
3298 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3737 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3299 3738 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3300 3739 $nodes[] = array(
3301 3740 'name' => $name,
3302 3741 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3303 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3742 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3304 3743 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3305 3744 'selectors' => $pseudo_feature_selectors,
3306 3745 'elements' => $selectors[ $name ]['elements'] ?? array(),
3307 3746 'variations' => $variation_selectors,
@@ -3352,33 +3791,56 @@
3352 3791 }
3353 3792 }
3354 3793 }
3355 3794 }
3356 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3357 - 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 ) {
3358 3812 $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3359 3813 if ( $include_node_paths_only ) {
3360 - $nodes[] = array(
3361 - 'path' => $element_path,
3362 - );
3814 + if ( isset( $block_node['elements'][ $element ] ) ) {
3815 + $nodes[] = array(
3816 + 'path' => $element_path,
3817 + );
3818 + }
3363 3819 continue;
3364 3820 }
3365 3821
3822 + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3823 + continue;
3824 + }
3825 +
3366 3826 $element_selector = $selectors[ $name ]['elements'][ $element ];
3367 3827
3368 - $nodes[] = array(
3369 - 'path' => $element_path,
3370 - 'selector' => $element_selector,
3371 - );
3828 + if ( isset( $block_node['elements'][ $element ] ) ) {
3829 + $nodes[] = array(
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3832 + );
3833 + }
3372 3834
3373 3835 // Responsive element nodes: one node per breakpoint that has
3374 3836 // styles for this element. Cascade: a{} → @media{a{}}
3375 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3837 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3376 3838 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3377 3839 $nodes[] = array(
3378 3840 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3379 3841 'selector' => $element_selector,
3380 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3842 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3381 3843 );
3382 3844 }
3383 3845 }
3384 3846
@@ -3384,45 +3846,29 @@
3384 3846
3385 3847 // Handle any pseudo selectors for the element.
3386 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3387 3849 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3388 - // Create element pseudo node if default or any responsive breakpoint has the pseudo.
3389 - $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] );
3390 - if ( ! $has_element_pseudo ) {
3391 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) {
3392 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3393 - $has_element_pseudo = true;
3394 - break;
3395 - }
3396 - }
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 + );
3397 3858 }
3398 3859
3399 - if ( $has_element_pseudo ) {
3400 - $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element );
3401 - 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 ] ) ) {
3402 3865 $nodes[] = array(
3403 - '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 ],
3404 3869 );
3405 - continue;
3406 3870 }
3407 -
3408 - $nodes[] = array(
3409 - 'path' => $element_pseudo_path,
3410 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3411 - );
3412 -
3413 - // Responsive element pseudo nodes: one node per breakpoint
3414 - // that has this pseudo state for this element.
3415 - // Cascade: a:hover{} → @media{a:hover{}}
3416 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3417 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3418 - $nodes[] = array(
3419 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3420 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3421 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3422 - );
3423 - }
3424 - }
3425 3871 }
3426 3872 }
3427 3873 }
3428 3874 }
@@ -3442,14 +3888,15 @@
3442 3888 *
3443 3889 * @return string Styles for the block.
3444 3890 */
3445 3891 public function get_styles_for_block( $block_metadata ) {
3446 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3447 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3448 - $selector = $block_metadata['selector'];
3449 - $settings = $this->theme_json['settings'] ?? null;
3450 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3451 - $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 );
3452 3899
3453 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3454 3901
3455 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3504,10 +3951,20 @@
3504 3951 // Store variation metadata and node for layout styles generation.
3505 3952 // Only store if the variation has blockGap defined.
3506 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3507 3954 // Append block selector to the variation selector for proper targeting.
3508 - $variation_metadata_with_selector = $style_variation;
3509 - $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 +
3510 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3511 3968 'metadata' => $variation_metadata_with_selector,
3512 3969 'node' => $style_variation_node,
3513 3970 );
@@ -3517,15 +3974,15 @@
3517 3974 // This includes both base properties and feature-level selectors.
3518 3975 $variation_responsive_css = '';
3519 3976 $variation_responsive_pseudo_css = '';
3520 3977
3521 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3978 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3522 3979 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3523 3980 continue;
3524 3981 }
3525 3982
3526 3983 $breakpoint_node = $style_variation_node[ $breakpoint ];
3527 - $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ];
3984 + $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3528 3985 // Process feature-level declarations for this breakpoint.
3529 3986 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3530 3987 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3531 3988 $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
@@ -3561,9 +4018,13 @@
3561 4018 // Process blockGap responsive layout styles for this variation.
3562 4019 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
3563 4020 $variation_layout_metadata = $style_variation;
3564 4021 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
3565 - $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(
3566 4027 $variation_layout_metadata,
3567 4028 array(
3568 4029 'node' => $breakpoint_node,
3569 4030 'media_query' => $breakpoint_media,
@@ -4226,9 +4687,9 @@
4226 4687 * Gets a `default`'s preset name by a provided slug.
4227 4688 *
4228 4689 * @since 5.9.0
4229 4690 *
4230 - * @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.
4231 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
4232 4693 * @return string|null
4233 4694 */
4234 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -4276,10 +4737,10 @@
4276 4737 * @since 5.9.0
4277 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
4278 4739 *
4279 4740 * @param array $theme_json Structure to sanitize.
4280 - * @param string $origin Optional. What source of data this object represents.
4281 - * 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'.
4282 4743 * @return array Sanitized structure.
4283 4744 */
4284 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4285 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -4299,11 +4760,12 @@
4299 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
4300 4761
4301 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4302 4763
4303 - $blocks_metadata = static::get_blocks_metadata();
4304 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4305 - $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 );
4306 4768
4307 4769 foreach ( $style_nodes as $metadata ) {
4308 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4309 4771 if ( empty( $input ) ) {
@@ -4339,18 +4801,18 @@
4339 4801 }
4340 4802 }
4341 4803
4342 4804 // Re-add and process responsive breakpoint styles.
4343 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4805 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4344 4806 if ( isset( $input[ $breakpoint ] ) ) {
4345 4807 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4346 4808
4347 4809 if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4348 - $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 );
4349 4811 }
4350 4812
4351 4813 if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4352 - $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 );
4353 4815 }
4354 4816
4355 4817 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4356 4818 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4380,26 +4842,26 @@
4380 4842
4381 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4382 4844
4383 4845 if ( isset( $variation_input['blocks'] ) ) {
4384 - $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 );
4385 4847 }
4386 4848
4387 4849 if ( isset( $variation_input['elements'] ) ) {
4388 - $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 );
4389 4851 }
4390 4852
4391 4853 // Re-add and process responsive breakpoint styles for variations.
4392 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4854 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4393 4855 if ( isset( $variation_input[ $breakpoint ] ) ) {
4394 4856 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4395 4857
4396 4858 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4397 - $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 );
4398 4860 }
4399 4861
4400 4862 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4401 - $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 );
4402 4864 }
4403 4865
4404 4866 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4405 4867 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4429,9 +4891,9 @@
4429 4891 if ( empty( $input ) ) {
4430 4892 continue;
4431 4893 }
4432 4894
4433 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4434 4896 if ( ! empty( $output ) ) {
4435 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4436 4898 }
4437 4899 }
@@ -4453,14 +4915,19 @@
4453 4915
4454 4916 /**
4455 4917 * Remove insecure element styles within a variation or block.
4456 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 + *
4457 4922 * @since 6.8.0
4458 4923 *
4459 - * @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.
4460 4927 * @return array The sanitized elements styles.
4461 4928 */
4462 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4463 4930 $sanitized = array();
4464 4931 $valid_element_names = array_keys( static::ELEMENTS );
4465 4932
4466 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4475,17 +4942,19 @@
4475 4942 }
4476 4943 }
4477 4944 }
4478 4945
4479 - // Re-add and process responsive breakpoint styles for elements.
4480 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4481 - if ( isset( $element_input[ $breakpoint ] ) ) {
4482 - $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 ] );
4483 4951
4484 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4485 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4486 - if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4487 - $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 + }
4488 4957 }
4489 4958 }
4490 4959 }
4491 4960 }
@@ -4499,31 +4968,38 @@
4499 4968
4500 4969 /**
4501 4970 * Remove insecure styles from inner blocks and their elements.
4502 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 + *
4503 4975 * @since 6.8.0
4504 4976 *
4505 - * @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.
4506 4980 * @return array Sanitized block type styles.
4507 4981 */
4508 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4509 4983 $sanitized = array();
4510 4984 foreach ( $blocks as $block_type => $block_input ) {
4511 4985 $block_output = static::remove_insecure_styles( $block_input );
4512 4986
4513 4987 if ( isset( $block_input['elements'] ) ) {
4514 - $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 );
4515 4989 }
4516 4990
4517 - // Re-add and process responsive breakpoint styles for inner blocks.
4518 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4519 - if ( isset( $block_input[ $breakpoint ] ) ) {
4520 - $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 ] );
4521 4996
4522 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4523 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4524 - if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4525 - $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 + }
4526 5002 }
4527 5003 }
4528 5004 }
4529 5005 }
@@ -4568,12 +5044,14 @@
4568 5044 * without the insecure settings.
4569 5045 *
4570 5046 * @since 5.9.0
4571 5047 *
4572 - * @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.
4573 5051 * @return array
4574 5052 */
4575 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4576 5054 $output = array();
4577 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4578 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4579 5057 $path_with_origin = $preset_metadata['path'];
@@ -4624,8 +5102,12 @@
4624 5102
4625 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4626 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4627 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4628 5110 return $output;
4629 5111 }
4630 5112
4631 5113 /**
@@ -5302,8 +5784,9 @@
5302 5784 * This is used to convert the internal representation of variables to the CSS representation.
5303 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
5304 5786 *
5305 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
5306 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
5307 5790 * @return string The converted variable.
5308 5791 */
5309 5792 private static function convert_custom_properties( $value ) {
@@ -5310,15 +5793,32 @@
5310 5793 $prefix = 'var:';
5311 5794 $prefix_len = strlen( $prefix );
5312 5795 $token_in = '|';
5313 5796 $token_out = '--';
5314 - if ( 0 === strpos( $value, $prefix ) ) {
5315 - $unwrapped_name = str_replace(
5316 - $token_in,
5317 - $token_out,
5318 - substr( $value, $prefix_len )
5319 - );
5320 - $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 ) . ')';
5321 5821 }
5322 5822
5323 5823 return $value;
5324 5824 }
@@ -5327,9 +5827,9 @@
5327 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
5328 5828 * It is recursive and modifies the input in-place.
5329 5829 *
5330 5830 * @since 6.3.0
5331 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
5332 5832 * @return array The modified $tree.
5333 5833 */
5334 5834 private static function resolve_custom_css_format( $tree ) {
5335 5835 $prefix = 'var:';
@@ -5334,9 +5834,9 @@
5334 5834 private static function resolve_custom_css_format( $tree ) {
5335 5835 $prefix = 'var:';
5336 5836
5337 5837 foreach ( $tree as $key => $data ) {
5338 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
5339 5839 $tree[ $key ] = self::convert_custom_properties( $data );
5340 5840 } elseif ( is_array( $data ) ) {
5341 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
5342 5842 }
@@ -5442,13 +5942,24 @@
5442 5942 $limit = 1;
5443 5943 $selector_parts = static::split_selector_list( $block_selector );
5444 5944 $result = array();
5445 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 + */
5446 5957 foreach ( $selector_parts as $part ) {
5447 5958 $result[] = preg_replace_callback(
5448 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5449 5960 function ( $matches ) use ( $variation_class ) {
5450 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5451 5962 },
5452 5963 $part,
5453 5964 $limit
5454 5965 );
@@ -5453,9 +5964,9 @@
5453 5964 $limit
5454 5965 );
5455 5966 }
5456 5967
5457 - return implode( ',', $result );
5968 + return implode( ', ', $result );
5458 5969 }
5459 5970
5460 5971 /**
5461 5972 * Applies a block style variation class to a feature selector.
@@ -5466,9 +5977,9 @@
5466 5977 * the variation class directly to the selector that will receive the
5467 5978 * declarations instead of deriving it by subtracting the root selector from
5468 5979 * the feature selector.
5469 5980 *
5470 - * @param array $style_variation Style variation metadata.
5981 + * @param array $style_variation Style variation metadata.
5471 5982 * @param string $feature_selector CSS selector for the feature.
5472 5983 * @return string Feature selector with block style variation selector added.
5473 5984 */
5474 5985 protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
@@ -5482,10 +5993,9 @@
5482 5993 $variation_class = ".is-style-$variation_name";
5483 5994 $selector_parts = static::split_selector_list( $feature_selector );
5484 5995 $selector_parts = array_map(
5485 5996 static function ( $selector ) use ( $variation_class ) {
5486 - $selector = trim( $selector );
5487 - $prefix = $variation_class . ' ';
5997 + $prefix = $variation_class . ' ';
5488 5998
5489 5999 if ( str_starts_with( $selector, $prefix ) ) {
5490 6000 return substr( $selector, strlen( $prefix ) );
5491 6001 }
@@ -5496,9 +6006,9 @@
5496 6006 );
5497 6007
5498 6008 return static::get_block_style_variation_selector(
5499 6009 $variation_name,
5500 - implode( ',', $selector_parts )
6010 + implode( ', ', $selector_parts )
5501 6011 );
5502 6012 }
5503 6013
5504 6014 /**