PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/class-wp-theme-json-gutenberg.php +713 -203 23.5.1 → trunk View file →
@@ -124,8 +124,9 @@
124 124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 125 * @since 6.2.0 Added 'shadow' presets.
126 126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 127 * @since 6.6.0 Added `aspectRatios`.
128 + * @since 7.2.0 Added 'textShadow' presets.
128 129 * @var array
129 130 */
130 131 const PRESETS_METADATA = array(
131 132 array(
@@ -186,8 +187,17 @@
186 187 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 188 'properties' => array( 'font-family' ),
188 189 ),
189 190 array(
191 + 'path' => array( 'typography', 'textShadowPresets' ),
192 + 'prevent_override' => array( 'typography', 'defaultTextShadowPresets' ),
193 + 'use_default_names' => false,
194 + 'value_key' => 'textShadow',
195 + 'css_vars' => '--wp--preset--text-shadow--$slug',
196 + 'classes' => array( '.has-$slug-text-shadow' => 'text-shadow' ),
197 + 'properties' => array( 'text-shadow' ),
198 + ),
199 + array(
190 200 'path' => array( 'spacing', 'spacingSizes' ),
191 201 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 202 'use_default_names' => true,
193 203 'value_key' => 'size',
@@ -240,9 +250,9 @@
240 250 * removed the `--wp--style--block-gap` property.
241 251 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 252 * @since 6.3.0 Added `writing-mode` property.
243 253 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
254 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
245 255 * `typography.textIndent` properties.
246 256 *
247 257 * @var array
248 258 */
@@ -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 /**