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 +728 -210 23.3.2 → 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 */
@@ -305,8 +315,9 @@
305 315 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 316 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 317 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 318 'text-decoration' => array( 'typography', 'textDecoration' ),
319 + 'text-shadow' => array( 'typography', 'textShadow' ),
309 320 'text-transform' => array( 'typography', 'textTransform' ),
310 321 'text-indent' => array( 'typography', 'textIndent' ),
311 322 'filter' => array( 'filter', 'duotone' ),
312 323 'box-shadow' => array( 'shadow' ),
@@ -388,10 +399,13 @@
388 399 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
389 400 * @since 6.4.0 Added `layout.allowEditing`.
390 401 * @since 6.4.0 Added `lightbox`.
391 402 * @since 7.0.0 Added type markers to the schema for boolean values.
392 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
403 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
393 404 * `typography.textIndent` properties.
405 + * @since 7.1.0 Added `viewport` property.
406 + * @since 7.2.0 Added `typography.textShadow`, `typography.textShadowPresets`,
407 + * and `typography.defaultTextShadowPresets`.
394 408 * @var array
395 409 */
396 410 const VALID_SETTINGS = array(
397 411 'appearanceTools' => null,
@@ -400,8 +414,11 @@
400 414 'backgroundImage' => null,
401 415 'backgroundSize' => null,
402 416 'gradient' => null,
403 417 ),
418 + 'blockVisibility' => array(
419 + 'allowEditing' => true,
420 + ),
404 421 'border' => array(
405 422 'color' => null,
406 423 'radius' => null,
407 424 'style' => null,
@@ -464,25 +481,32 @@
464 481 'presets' => null,
465 482 'defaultPresets' => null,
466 483 ),
467 484 'typography' => array(
468 - 'fluid' => null,
469 - 'customFontSize' => null,
470 - 'defaultFontSizes' => null,
471 - 'dropCap' => null,
472 - 'fontFamilies' => null,
473 - 'fontSizes' => null,
474 - 'fontStyle' => null,
475 - 'fontWeight' => null,
476 - 'letterSpacing' => null,
477 - 'lineHeight' => null,
478 - 'textAlign' => null,
479 - 'textColumns' => null,
480 - 'textDecoration' => null,
481 - 'textIndent' => null,
482 - 'textTransform' => null,
483 - 'writingMode' => null,
485 + 'fluid' => null,
486 + 'customFontSize' => null,
487 + 'defaultFontSizes' => null,
488 + 'dropCap' => null,
489 + 'fontFamilies' => null,
490 + 'fontSizes' => null,
491 + 'fontStyle' => null,
492 + 'fontWeight' => null,
493 + 'letterSpacing' => null,
494 + 'lineHeight' => null,
495 + 'textAlign' => null,
496 + 'textColumns' => null,
497 + 'textDecoration' => null,
498 + 'textIndent' => null,
499 + 'textTransform' => null,
500 + 'textShadow' => null,
501 + 'defaultTextShadowPresets' => null,
502 + 'textShadowPresets' => null,
503 + 'writingMode' => null,
484 504 ),
505 + 'viewport' => array(
506 + 'mobile' => null,
507 + 'tablet' => null,
508 + ),
485 509 );
486 510
487 511 const FONT_FAMILY_SCHEMA = array(
488 512 array(
@@ -521,9 +545,9 @@
521 545 * updated `blockGap` to be allowed at any level.
522 546 * @since 6.2.0 Added `outline`, and `minHeight` properties.
523 547 * @since 6.6.0 Added `background` sub properties to top-level only.
524 548 * @since 6.6.0 Added `dimensions.aspectRatio`.
525 - * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
549 + * @since 7.0.0 Added `dimensions.width`, `dimensions.height`, and
526 550 * `typography.textIndent` properties.
527 551 * @var array
528 552 */
529 553 const VALID_STYLES = array(
@@ -582,8 +606,9 @@
582 606 'textAlign' => null,
583 607 'textColumns' => null,
584 608 'textDecoration' => null,
585 609 'textIndent' => null,
610 + 'textShadow' => null,
586 611 'textTransform' => null,
587 612 'writingMode' => null,
588 613 ),
589 614 'css' => null,
@@ -620,29 +645,187 @@
620 645 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
621 646 );
622 647
623 648 /**
624 - * Responsive breakpoint state keys and their corresponding CSS media queries.
625 - * These are available for all blocks and wrap their styles in the given media query.
626 - * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx.
649 + * Default viewport breakpoint sizes.
627 650 *
628 651 * @since 7.1.0
629 652 * @var array
630 653 */
631 - const RESPONSIVE_BREAKPOINTS = array(
632 - 'mobile' => '@media (width <= 480px)',
633 - 'tablet' => '@media (480px < width <= 782px)',
654 + const DEFAULT_VIEWPORT_BREAKPOINTS = array(
655 + 'mobile' => '480px',
656 + 'tablet' => '782px',
634 657 );
635 658
636 659 /**
660 + * Returns CSS media queries for responsive viewport style states.
661 + *
662 + * Breakpoint values are read from `settings.viewport`, sanitized, and
663 + * normalized before the media query strings are generated. By default, the
664 + * returned keys are the theme.json style-state names (`@mobile`, `@tablet`).
665 + * When `$options['include_desktop']` is truthy, `@desktop` is included.
666 + *
667 + * @since 7.1.0
668 + *
669 + * @param mixed $viewport_settings Viewport settings from theme.json.
670 + * @param array $options {
671 + * Optional. Options for generating media queries.
672 + *
673 + * @type bool $include_desktop Whether to include the desktop media query. Default false.
674 + * }
675 + * @return array Responsive media queries.
676 + */
677 + public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) {
678 + $breakpoints = static::sanitize_viewport_settings( $viewport_settings );
679 +
680 + $responsive_media_queries = array();
681 +
682 + if ( isset( $breakpoints['mobile'] ) ) {
683 + $responsive_media_queries['@mobile'] = "@media (width <= {$breakpoints['mobile']})";
684 + }
685 +
686 + if ( isset( $breakpoints['tablet'] ) ) {
687 + $responsive_media_queries['@tablet'] = isset( $breakpoints['mobile'] )
688 + ? sprintf(
689 + '@media (%s < width <= %s)',
690 + $breakpoints['mobile'],
691 + $breakpoints['tablet']
692 + )
693 + : "@media (width <= {$breakpoints['tablet']})";
694 + }
695 +
696 + if ( ! empty( $options['include_desktop'] ) ) {
697 + if ( isset( $breakpoints['tablet'] ) ) {
698 + $desktop_breakpoint = $breakpoints['tablet'];
699 + } else {
700 + $desktop_breakpoint = $breakpoints['mobile'];
701 + }
702 +
703 + $responsive_media_queries['@desktop'] =
704 + "@media (width > {$desktop_breakpoint})";
705 + }
706 +
707 + return $responsive_media_queries;
708 + }
709 +
710 + /**
711 + * Checks whether a viewport breakpoint value is a safe CSS length.
712 + *
713 + * Viewport breakpoints are limited to numeric `px`, `em`, and `rem` lengths.
714 + * CSS functions, percentages, and other units are rejected because breakpoint
715 + * values are interpolated into generated media queries.
716 + *
717 + * @since 7.1.0
718 + *
719 + * @param mixed $value Value to check.
720 + * @return bool Whether the value is valid.
721 + */
722 + private static function is_valid_viewport_breakpoint_size( $value ) {
723 + if ( ! is_string( $value ) ) {
724 + return false;
725 + }
726 +
727 + $value = trim( $value );
728 + if ( '' === $value ) {
729 + return false;
730 + }
731 +
732 + return 1 === preg_match( '/^(?:\d+|\d*\.\d+)(?:px|em|rem)$/', $value );
733 + }
734 +
735 + /**
736 + * Converts a valid viewport breakpoint size to pixels for ordering checks.
737 + *
738 + * Generated media queries keep the original units. This method only
739 + * normalizes values so `mobile` and `tablet` can be compared safely. `em`
740 + * and `rem` lengths use a 16px base for comparison.
741 + *
742 + * @since 7.1.0
743 + *
744 + * @param mixed $value Viewport breakpoint size.
745 + * @return float|null Viewport breakpoint size in pixels, or null when invalid.
746 + */
747 + private static function get_viewport_breakpoint_value_in_pixels( $value ) {
748 + if ( ! static::is_valid_viewport_breakpoint_size( $value ) ) {
749 + return null;
750 + }
751 +
752 + $value = trim( $value );
753 + $unit = substr( $value, -3 );
754 + if ( 'rem' === $unit ) {
755 + $number = (float) substr( $value, 0, -3 );
756 + } else {
757 + $unit = substr( $value, -2 );
758 + $number = (float) substr( $value, 0, -2 );
759 + }
760 +
761 + /*
762 + * Use the most common browser default font size as the base for em/rem
763 + * media query conversions. This pixel value is only used to compare
764 + * breakpoint order; generated media queries keep the original units.
765 + */
766 + return 'px' === $unit ? $number : $number * 16;
767 + }
768 +
769 + /**
770 + * Sanitizes and normalizes viewport breakpoint settings.
771 + *
772 + * Keeps only supported breakpoint keys, trims valid CSS lengths, and returns
773 + * the default breakpoints when no valid custom breakpoint is provided. When
774 + * only one breakpoint is valid, it remains keyed by its configured state and
775 + * uses a single max-width media query. When `tablet` is not larger than
776 + * `mobile`, it is removed.
777 + *
778 + * @since 7.1.0
779 + *
780 + * @param mixed $viewport_settings Viewport settings from theme.json.
781 + * @return array Sanitized viewport breakpoint settings.
782 + */
783 + private static function sanitize_viewport_settings( $viewport_settings ) {
784 + if ( ! is_array( $viewport_settings ) ) {
785 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
786 + }
787 +
788 + $breakpoints = array();
789 + foreach ( array_keys( static::DEFAULT_VIEWPORT_BREAKPOINTS ) as $breakpoint ) {
790 + $value = $viewport_settings[ $breakpoint ] ?? null;
791 + $px = static::get_viewport_breakpoint_value_in_pixels( $value );
792 + if ( null !== $px ) {
793 + $breakpoints[ $breakpoint ] = array(
794 + 'value' => trim( $value ),
795 + 'px' => $px,
796 + );
797 + }
798 + }
799 +
800 + if ( empty( $breakpoints ) ) {
801 + return static::DEFAULT_VIEWPORT_BREAKPOINTS;
802 + }
803 +
804 + if ( 1 === count( $breakpoints ) ) {
805 + $breakpoint = key( $breakpoints );
806 + return array( $breakpoint => $breakpoints[ $breakpoint ]['value'] );
807 + }
808 +
809 + $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] );
810 +
811 + if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px']
812 + ) {
813 + $sanitized['tablet'] = $breakpoints['tablet']['value'];
814 + }
815 +
816 + return $sanitized;
817 + }
818 +
819 + /**
637 820 * Custom states for blocks that map to CSS class selectors rather than
638 - * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
639 - * distinguish them from real CSS pseudo-selectors.
821 + * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
822 + * distinguish them from real CSS pseudo-selectors and breakpoint states.
640 823 *
641 824 * The CSS selector for each state is defined in the block's block.json
642 825 * under `selectors.states`, e.g.:
643 826 *
644 - * "selectors": { "states": { "@current": ".some-css-selector" } }
827 + * "selectors": { "states": { "-current": ".some-css-selector" } }
645 828 *
646 829 * This constant controls which states are valid in theme.json for a given
647 830 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
648 831 * as valid sub-states, producing compound selectors such as
@@ -650,9 +833,9 @@
650 833 *
651 834 * @var array
652 835 */
653 836 const VALID_BLOCK_CUSTOM_STATES = array(
654 - 'core/navigation-link' => array( '@current' ),
837 + 'core/navigation-link' => array( '-current' ),
655 838 );
656 839
657 840 /**
658 841 * The valid elements that can be found under styles.
@@ -674,8 +857,9 @@
674 857 'button' => '.wp-element-button, .wp-block-button__link',
675 858 // The block classes are necessary to target older content that won't use the new class names.
676 859 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
677 860 'cite' => 'cite',
861 + 'label' => 'label',
678 862 'select' => 'select',
679 863 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
680 864 );
681 865
@@ -747,13 +931,15 @@
747 931
748 932 /**
749 933 * Processes pseudo-selectors for any node (block or variation).
750 934 *
751 - * @param array $node The node data (block or variation).
752 - * @param string $base_selector The base selector.
753 - * @param array $settings The theme settings.
754 - * @param string $block_name The block name.
755 - * @param array|null $block_metadata Metadata about the block to get styles for.
935 + * @since 7.0.0
936 + *
937 + * @param array $node The node data (block or variation).
938 + * @param string $base_selector The base selector.
939 + * @param array $settings The theme settings.
940 + * @param string $block_name The block name.
941 + * @param array|null $block_metadata Metadata about the block to get styles for.
756 942 * @param array|null $style_variation Style variation metadata.
757 943 * @return array Array of pseudo-selector declarations.
758 944 */
759 945 private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
@@ -887,9 +1073,12 @@
887 1073 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
888 1074 $origin = 'theme';
889 1075 }
890 1076
891 - $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
1077 + $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
1078 + if ( isset( $this->theme_json['styles'] ) ) {
1079 + $this->theme_json['styles'] = gutenberg_resolve_style_state_aliases( $this->theme_json['styles'] );
1080 + }
892 1081 $blocks_metadata = static::get_blocks_metadata();
893 1082 $valid_block_names = array_keys( $blocks_metadata );
894 1083 $valid_element_names = array_keys( static::ELEMENTS );
895 1084 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
@@ -1058,8 +1247,10 @@
1058 1247 *
1059 1248 * @since 5.8.0
1060 1249 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1061 1250 * @since 6.6.0 Extended schema definition to allow enhanced block style variations.
1251 + * @since 7.1.1 Updated schema to allow responsive breakpoint states and pseudo-selectors
1252 + * at the top level of `styles` for block style variation partials.
1062 1253 *
1063 1254 * @param array $input Structure to sanitize.
1064 1255 * @param array $valid_block_names List of valid block names.
1065 1256 * @param array $valid_element_names List of valid element names.
@@ -1094,10 +1285,11 @@
1094 1285 }
1095 1286 }
1096 1287
1097 1288 // Build the schema based on valid block & element names.
1098 - $schema = array();
1099 - $schema_styles_elements = array();
1289 + $schema = array();
1290 + $schema_styles_elements = array();
1291 + $responsive_media_queries = static::get_viewport_media_queries( $input['settings']['viewport'] ?? null );
1100 1292
1101 1293 /*
1102 1294 * Set allowed element pseudo selectors and responsive breakpoint states.
1103 1295 * Target data structure in schema:
@@ -1103,9 +1295,9 @@
1103 1295 * Target data structure in schema:
1104 1296 * e.g.
1105 1297 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1106 1298 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1107 - * - block responsive elements: `$schema['styles']['blocks']['core/button']['tablet']['elements']['link'][':hover']`.
1299 + * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1108 1300 */
1109 1301 foreach ( $valid_element_names as $element ) {
1110 1302 $schema_styles_elements[ $element ] = $styles_non_top_level;
1111 1303
@@ -1115,9 +1307,9 @@
1115 1307 }
1116 1308 }
1117 1309
1118 1310 // Add responsive breakpoint states for elements.
1119 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1311 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1120 1312 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1121 1313 }
1122 1314 }
1123 1315
@@ -1122,9 +1314,17 @@
1122 1314 }
1123 1315
1124 1316 $schema_styles_blocks = array();
1125 1317 $schema_settings_blocks = array();
1318 + $breakpoint_states = array_keys( $responsive_media_queries );
1126 1319
1320 + $common_block_settings = static::VALID_SETTINGS;
1321 + // `viewport` and `blockVisibility` are global-only settings and cannot be set per block for now.
1322 + unset(
1323 + $common_block_settings['viewport'],
1324 + $common_block_settings['blockVisibility']
1325 + );
1326 +
1127 1327 /*
1128 1328 * Generate a schema for blocks.
1129 1329 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1130 1330 * - Variations definitions cannot be nested.
@@ -1132,20 +1332,29 @@
1132 1332 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1133 1333 *
1134 1334 * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1135 1335 * for further nested inner `blocks`, the overall schema is generated in multiple passes.
1336 + *
1337 + * All blocks start with the same style schema. Build that common schema
1338 + * once, then add block-specific pseudo and custom states below.
1136 1339 */
1340 + $responsive_block_schema = $styles_non_top_level;
1341 + $responsive_block_schema['elements'] = $schema_styles_elements;
1342 +
1343 + $common_block_schema = $styles_non_top_level;
1344 + $common_block_schema['elements'] = $schema_styles_elements;
1345 +
1346 + foreach ( $breakpoint_states as $breakpoint_state ) {
1347 + $common_block_schema[ $breakpoint_state ] = $responsive_block_schema;
1348 + }
1349 +
1137 1350 foreach ( $valid_block_names as $block ) {
1138 - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1139 - $schema_styles_blocks[ $block ] = $styles_non_top_level;
1140 - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1351 + $schema_settings_blocks[ $block ] = $common_block_settings;
1352 + $schema_styles_blocks[ $block ] = $common_block_schema;
1141 1353
1142 - // Add responsive breakpoint states for all blocks.
1143 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1144 - $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level;
1145 - $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1146 -
1147 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1354 + // Add responsive pseudo-selectors only to blocks that support them.
1355 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1356 + foreach ( $breakpoint_states as $breakpoint_state ) {
1148 1357 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1149 1358 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1150 1359 }
1151 1360 }
@@ -1157,9 +1366,9 @@
1157 1366 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1158 1367 }
1159 1368 }
1160 1369
1161 - // Add custom states for blocks that support them (e.g. '@current' for navigation).
1370 + // Add custom states for blocks that support them (e.g. '-current' for navigation).
1162 1371 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1163 1372 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1164 1373 $custom_state_schema = $styles_non_top_level;
1165 1374 // The same pseudo-selectors valid for the block at the top level
@@ -1197,12 +1406,11 @@
1197 1406 foreach ( $style_variation_names as $variation_name ) {
1198 1407 $variation_schema = $block_style_variation_styles;
1199 1408
1200 1409 // Add responsive breakpoint states to block style variations.
1201 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) {
1410 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1202 1411 $variation_schema[ $breakpoint_state ] = $styles_non_top_level;
1203 1412 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1204 - $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks;
1205 1413
1206 1414 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1207 1415 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1208 1416 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
@@ -1230,8 +1438,49 @@
1230 1438 $schema['settings'] = static::VALID_SETTINGS;
1231 1439 $schema['settings']['blocks'] = $schema_settings_blocks;
1232 1440 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1233 1441
1442 + /*
1443 + * Add block style variation states to the top-level styles schema.
1444 + *
1445 + * Block style variations defined in a standalone JSON partial within a
1446 + * theme's `styles` directory declare their styles at the root of the
1447 + * `styles` object, so they are sanitized against the top-level schema.
1448 + * It needs to allow the same states that are allowed for variations
1449 + * declared inline in theme.json, otherwise those states are silently
1450 + * removed as unknown keys.
1451 + *
1452 + * The `blockTypes` property is only present on block style variation
1453 + * partials, so it both identifies the config as a variation and
1454 + * determines which pseudo-selectors are valid for it. Regular
1455 + * theme.json files are unaffected.
1456 + */
1457 + if ( ! empty( $input['blockTypes'] ) && is_array( $input['blockTypes'] ) ) {
1458 + $variation_pseudo_selectors = array();
1459 + foreach ( $input['blockTypes'] as $variation_block_type ) {
1460 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] ) ) {
1461 + $variation_pseudo_selectors = array_merge(
1462 + $variation_pseudo_selectors,
1463 + static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ]
1464 + );
1465 + }
1466 + }
1467 + $variation_pseudo_selectors = array_unique( $variation_pseudo_selectors );
1468 +
1469 + foreach ( $breakpoint_states as $breakpoint_state ) {
1470 + $schema['styles'][ $breakpoint_state ] = $styles_non_top_level;
1471 + $schema['styles'][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1472 +
1473 + foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1474 + $schema['styles'][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1475 + }
1476 + }
1477 +
1478 + foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1479 + $schema['styles'][ $pseudo_selector ] = $styles_non_top_level;
1480 + }
1481 + }
1482 +
1234 1483 // Remove anything that's not present in the schema.
1235 1484 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1236 1485 if ( ! isset( $input[ $subtree ] ) ) {
1237 1486 continue;
@@ -1243,8 +1492,12 @@
1243 1492 }
1244 1493
1245 1494 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1246 1495
1496 + if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1497 + $result['viewport'] = static::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1498 + }
1499 +
1247 1500 if ( empty( $result ) ) {
1248 1501 unset( $output[ $subtree ] );
1249 1502 } else {
1250 1503 $output[ $subtree ] = static::resolve_custom_css_format( $result );
@@ -1272,14 +1525,38 @@
1272 1525 protected static function append_to_selector( $selector, $to_append ) {
1273 1526 if ( ! str_contains( $selector, ',' ) ) {
1274 1527 return $selector . $to_append;
1275 1528 }
1529 +
1530 + /**
1531 + * Check for an opportunity to skip the more-costly selector splitting.
1532 + * This should be possible if there are no comments, strings, functions,
1533 + * URLs, escapes, or comment declaration openers (CDOs).
1534 + *
1535 + * Note that this means the fast-path will not apply for selectors like
1536 + * the following incomplete list:
1537 + *
1538 + * - `[class ~= "wide"]`
1539 + * - `.wp-block:is(.is-style-a, .is-style-b)`
1540 + * - `:nth-child(1)`
1541 + *
1542 + * These syntax forms all present opportunities where a comma may not
1543 + * separate selectors. If none of the start characters are present,
1544 + * there should be no way for a comma to mean anything other than a
1545 + * comma token. The exception are syntax errors, which are not handled here.
1546 + *
1547 + * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1548 + */
1549 + if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1550 + return str_replace( ',', $to_append . ',', $selector ) . $to_append;
1551 + }
1552 +
1276 1553 $new_selectors = array();
1277 1554 $selectors = static::split_selector_list( $selector );
1278 1555 foreach ( $selectors as $sel ) {
1279 1556 $new_selectors[] = $sel . $to_append;
1280 1557 }
1281 - return implode( ',', $new_selectors );
1558 + return implode( ', ', $new_selectors );
1282 1559 }
1283 1560
1284 1561 /**
1285 1562 * Prepends a sub-selector to an existing one.
@@ -1297,49 +1574,194 @@
1297 1574 protected static function prepend_to_selector( $selector, $to_prepend ) {
1298 1575 if ( ! str_contains( $selector, ',' ) ) {
1299 1576 return $to_prepend . $selector;
1300 1577 }
1578 +
1579 + /**
1580 + * Check for an opportunity to skip the more-costly selector splitting.
1581 + * This should be possible if there are no comments, strings, functions,
1582 + * URLs, escapes, or comment declaration openers (CDOs).
1583 + *
1584 + * Note that this means the fast-path will not apply for selectors like
1585 + * the following incomplete list:
1586 + *
1587 + * - `[class ~= "wide"]`
1588 + * - `.wp-block:is(.is-style-a, .is-style-b)`
1589 + * - `:nth-child(1)`
1590 + *
1591 + * These syntax forms all present opportunities where a comma may not
1592 + * separate selectors. If none of the start characters are present,
1593 + * there should be no way for a comma to mean anything other than a
1594 + * comma token. The exception are syntax errors, which are not handled here.
1595 + *
1596 + * @link https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1597 + */
1598 + if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1599 + return $to_prepend . str_replace( ',', ',' . $to_prepend, $selector );
1600 + }
1601 +
1301 1602 $new_selectors = array();
1302 1603 $selectors = static::split_selector_list( $selector );
1303 1604 foreach ( $selectors as $sel ) {
1304 1605 $new_selectors[] = $to_prepend . $sel;
1305 1606 }
1306 - return implode( ',', $new_selectors );
1607 +
1608 + return implode( ', ', $new_selectors );
1307 1609 }
1308 1610
1309 1611 /**
1310 - * Splits a selector list by top-level commas.
1612 + * Splits a selector list into separate selectors.
1311 1613 *
1614 + * While selectors are joined by commas, not all commas separate top-level selectors.
1615 + * This method only separates top-level selectors, so some commas may appear inside
1616 + * strings, nested selectors, and comments. Leading and trailing CSS whitespace is
1617 + * trimmed from the returned list items.
1618 + *
1619 + * Non-selector content, such as comments, are retained in the list in the same item
1620 + * as the selector content they follow.
1621 + *
1622 + * Example:
1623 + *
1624 + * array( '.wp-block' ) === self::split_selector_list( '.wp-block' );
1625 + * array( '.one', '.two' ) === self::split_selector_list( '.one, .two' );
1626 + *
1627 + * // Nested selector lists are retained within their containing selector.
1628 + * array( ':is(.a, .b)', 'c' ) === self::split_selector_list( ':is(.a, .b), .c' );
1629 + *
1630 + * // Commas within strings do not separate selectors.
1631 + * $selectors = self::split_selector_list( '[data-label="Save, continue"],.fallback' );
1632 + * $selectors === array( '[data-label="Save, continue"]', '.fallback' )
1633 + *
1634 + * array( 'lang(zh, "*-hant")', '.foo' ) === self::split_selector_list( 'lang(zh, "*-hant"), .foo' );
1635 + *
1636 + * // Identifiers may contain escaped commas.
1637 + * array( '.foo\,bar', '.baz' ) === self::split_selector_list( '.foo\,bar,.baz' );
1638 + *
1639 + * // Comments stay with the selector they follow.
1640 + * array( '.a /* a, the first *\/', '.b' ) === self::split_selector_list( '.a /* a, the first *\/,.b' );
1641 + *
1642 + * @link https://www.w3.org/TR/selectors/#parse-selector
1643 + * @link https://www.w3.org/TR/css-syntax-3/
1644 + *
1312 1645 * @param string $selector CSS selector list.
1313 1646 * @return string[] Selectors.
1314 1647 */
1315 - protected static function split_selector_list( $selector ) {
1648 + protected static function split_selector_list( $selector ): array {
1316 1649 if ( ! str_contains( $selector, ',' ) ) {
1317 - return array( $selector );
1650 + // See note on trimming CSS whitespace in main loop.
1651 + return array( trim( $selector, " \t\n" ) );
1318 1652 }
1319 1653
1320 1654 $selectors = array();
1321 - $current_selector = '';
1655 + $selector_length = strlen( $selector );
1322 1656 $parentheses_depth = 0;
1323 - $selector_length = strlen( $selector );
1657 + $at = 0;
1658 + $was_at = 0;
1324 1659
1325 - for ( $i = 0; $i < $selector_length; $i++ ) {
1326 - $char = $selector[ $i ];
1660 + while ( $at < $selector_length ) {
1661 + $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1662 + if ( $next_at >= $selector_length ) {
1663 + break;
1664 + }
1327 1665
1328 - if ( '(' === $char ) {
1329 - ++$parentheses_depth;
1330 - } elseif ( ')' === $char && $parentheses_depth > 0 ) {
1331 - --$parentheses_depth;
1332 - } elseif ( ',' === $char && 0 === $parentheses_depth ) {
1333 - $selectors[] = $current_selector;
1334 - $current_selector = '';
1666 + $next_cp = $selector[ $next_at ];
1667 +
1668 + // Escaped syntax characters do not act as delimiters.
1669 + if ( '\\' === $next_cp ) {
1670 + $at = min( $next_at + 2, $selector_length );
1335 1671 continue;
1336 1672 }
1337 1673
1338 - $current_selector .= $char;
1674 + /*
1675 + * Start of a parenthesized expression, which maintains a stack of parentheses.
1676 + * For the sake of this function, no selector list will be split inside parentheses.
1677 + * Therefore it’s possible to jump ahead until this list completes.
1678 + */
1679 + if ( '(' === $next_cp || ')' === $next_cp ) {
1680 + $parentheses_depth += '(' === $next_cp ? 1 : -1;
1681 + $at = $next_at + 1;
1682 + continue;
1683 + }
1684 +
1685 + // Start of a string, which will be incorporated into the selector in which it’s found.
1686 + if ( "'" === $next_cp || '"' === $next_cp ) {
1687 + $end_of_string = $next_at + 1;
1688 + while ( $end_of_string < $selector_length ) {
1689 + $end_of_string += strcspn( $selector, "{$next_cp}\\", $end_of_string );
1690 + if ( $end_of_string >= $selector_length ) {
1691 + break;
1692 + }
1693 +
1694 + $end_cp = $selector[ $end_of_string ];
1695 +
1696 + // Skip escaped characters.
1697 + if ( '\\' === $end_cp ) {
1698 + $end_of_string = $end_of_string + 2;
1699 + continue;
1700 + }
1701 +
1702 + if ( $next_cp === $end_cp ) {
1703 + ++$end_of_string;
1704 + break;
1705 + }
1706 +
1707 + ++$end_of_string;
1708 + }
1709 +
1710 + $at = $end_of_string;
1711 + continue;
1712 + }
1713 +
1714 + // Start of a comment, which will be incorporated into the selector in which it’s found.
1715 + if ( '/' === $next_cp && ( $next_at + 1 ) < $selector_length && '*' === $selector[ $next_at + 1 ] ) {
1716 + $comment_end_at = strpos( $selector, '*/', $next_at + 1 );
1717 + $is_terminated = false !== $comment_end_at;
1718 + $after_comment = $is_terminated ? $comment_end_at + 2 : strlen( $selector );
1719 + $at = $after_comment;
1720 + continue;
1721 + }
1722 +
1723 + // Start of a CDO or CDC, which will be incorporated into the selector in which it’s found.
1724 + if (
1725 + ( '<' === $next_cp && 0 === substr_compare( $selector, '<!--', $next_at, 4 ) ) ||
1726 + ( '-' === $next_cp && 0 === substr_compare( $selector, '-->', $next_at, 3 ) )
1727 + ) {
1728 + $at = $next_at + ( '<' === $next_cp ? 4 : 3 );
1729 + continue;
1730 + }
1731 +
1732 + // Everything else is either a comma token or part of a selector.
1733 + if ( ',' === $next_cp && 0 === $parentheses_depth ) {
1734 + /**
1735 + * Trim each selector so that downstream code doesn’t see whitespace
1736 + * as the first character in a selector and get confused.
1737 + *
1738 + * There is inconsistency in this because comments and other syntax
1739 + * are included which are also not part of the selector itself, but
1740 + * a tradeoff is made between removing common syntax which carries
1741 + * no meaning and rarer syntax which leaves auxiliary information.
1742 + *
1743 + * > A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
1744 + * > Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are
1745 + * > not included in this definition, as they are converted
1746 + * > to U+000A LINE FEED during preprocessing.
1747 + *
1748 + * @link https://www.w3.org/TR/css-syntax/#whitespace
1749 + * @link https://www.w3.org/TR/css-syntax/#newline
1750 + */
1751 + $selectors[] = trim( substr( $selector, $was_at, $next_at - $was_at ), " \t\n" );
1752 + $at = $next_at + 1;
1753 + $was_at = $at;
1754 + continue;
1755 + }
1756 +
1757 + $at = $next_at + 1;
1339 1758 }
1340 1759
1341 - $selectors[] = $current_selector;
1760 + if ( $was_at < $selector_length ) {
1761 + // See note on trimming CSS whitespace in main loop.
1762 + $selectors[] = trim( substr( $selector, $was_at ), " \t\n" );
1763 + }
1342 1764
1343 1765 return $selectors;
1344 1766 }
1345 1767
@@ -1761,9 +2183,9 @@
1761 2183 /**
1762 2184 * Returns the global styles custom CSS for a single block.
1763 2185 * This function is deprecated; please do not sync to core.
1764 2186 *
1765 - * @param array $css The block css node.
2187 + * @param array $css The block css node.
1766 2188 * @param string $selector The block selector.
1767 2189 *
1768 2190 * @return string The global styles custom CSS for the block.
1769 2191 */
@@ -1888,9 +2310,10 @@
1888 2310
1889 2311 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1890 2312 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1891 2313 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1892 - $block_gap_value = null;
2314 + $block_gap_value = null;
2315 + $block_gap_row_value = null;
1893 2316 // Use a fallback gap value if block gap support is not available.
1894 2317 if ( ! $has_block_gap_support ) {
1895 2318 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1896 2319 if ( ! empty( $block_type ) ) {
@@ -1898,18 +2321,29 @@
1898 2321 }
1899 2322 } else {
1900 2323 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1901 2324 }
2325 + $block_gap_row_value = $block_gap_value;
1902 2326
1903 2327 // Support split row / column values and concatenate to a shorthand value.
1904 2328 if ( is_array( $block_gap_value ) ) {
1905 - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1906 - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1907 - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1908 - $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
2329 + $has_block_gap_row_value = isset( $block_gap_value['top'] );
2330 + $has_block_gap_column_value = isset( $block_gap_value['left'] );
2331 +
2332 + if ( $has_block_gap_row_value || $has_block_gap_column_value ) {
2333 + $block_gap_row_value = $has_block_gap_row_value
2334 + ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) )
2335 + : '0';
2336 + $block_gap_column_value = $has_block_gap_column_value
2337 + ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) )
2338 + : '0';
2339 + $block_gap_value = $block_gap_row_value === $block_gap_column_value
2340 + ? $block_gap_row_value
2341 + : $block_gap_row_value . ' ' . $block_gap_column_value;
1909 2342 } else {
1910 - // Skip outputting gap value if not all sides are provided.
1911 - $block_gap_value = null;
2343 + // Skip outputting a gap value if neither supported axis is provided.
2344 + $block_gap_value = null;
2345 + $block_gap_row_value = null;
1912 2346 }
1913 2347 }
1914 2348
1915 2349 // If the block should have custom gap, add the gap styles.
@@ -1919,10 +2353,13 @@
1919 2353 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1920 2354 continue;
1921 2355 }
1922 2356
1923 - $class_name = $layout_definition['className'] ?? false;
1924 - $spacing_rules = $layout_definition['spacingStyles'] ?? array();
2357 + $class_name = $layout_definition['className'] ?? false;
2358 + $spacing_rules = $layout_definition['spacingStyles'] ?? array();
2359 + $layout_gap_value = in_array( $layout_definition_key, array( 'default', 'constrained' ), true )
2360 + ? $block_gap_row_value
2361 + : $block_gap_value;
1925 2362
1926 2363 if (
1927 2364 ! empty( $class_name ) &&
1928 2365 ! empty( $spacing_rules )
@@ -1935,9 +2372,9 @@
1935 2372 ! empty( $spacing_rule['rules'] )
1936 2373 ) {
1937 2374 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1938 2375 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1939 - $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
2376 + $current_css_value = is_string( $css_value ) ? $css_value : $layout_gap_value;
1940 2377 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1941 2378 $declarations[] = array(
1942 2379 'name' => $css_property,
1943 2380 'value' => $current_css_value,
@@ -2076,9 +2513,9 @@
2076 2513 * .has-value-gradient-background {
2077 2514 * background: value;
2078 2515 * }
2079 2516 *
2080 - * p.has-value-gradient-background {
2517 + * :where(p).has-value-gradient-background {
2081 2518 * background: value;
2082 2519 * }
2083 2520 *
2084 2521 * @since 5.9.0
@@ -2279,10 +2716,18 @@
2279 2716 foreach ( $slugs as $slug ) {
2280 2717 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2281 2718 $class_name = static::replace_slug_in_string( $class, $slug );
2282 2719
2283 - // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2284 - $new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
2720 + /*
2721 + * $selector is often empty (root-level presets), in which case the
2722 + * bare class is used. For block-level presets the block selector is
2723 + * wrapped in `:where()` so the class keeps the same 0-1-0 specificity
2724 + * as a root-level preset. Without this, block-level palette rules
2725 + * (e.g. `p.has-x-color`) out-rank equally-important rules that also
2726 + * target the same property at 0-1-0, such as per-instance responsive
2727 + * state styles.
2728 + */
2729 + $new_selector = '' === $selector ? $class_name : ':where(' . $selector . ')' . $class_name;
2285 2730 $stylesheet .= static::to_ruleset(
2286 2731 $new_selector,
2287 2732 array(
2288 2733 array(
@@ -2325,10 +2770,8 @@
2325 2770
2326 2771 $selectors_scoped = array();
2327 2772 foreach ( $scopes as $outer ) {
2328 2773 foreach ( $selectors as $inner ) {
2329 - $outer = trim( $outer );
2330 - $inner = trim( $inner );
2331 2774 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2332 2775 $selectors_scoped[] = $outer . ' ' . $inner;
2333 2776 } elseif ( empty( $outer ) ) {
2334 2777 $selectors_scoped[] = $inner;
@@ -2634,15 +3077,15 @@
2634 3077 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2635 3078 * @since 6.6.0 Passing current theme JSON settings to wp_get_typography_font_size_value(). Using style engine to correctly fetch background CSS values.
2636 3079 * @since 6.7.0 Allow ref resolution of background properties.
2637 3080 *
2638 - * @param array $styles Styles to process.
2639 - * @param array $settings Theme settings.
2640 - * @param array $properties Properties metadata.
2641 - * @param array $theme_json Theme JSON array.
2642 - * @param string $selector The style block selector.
3081 + * @param array $styles Styles to process.
3082 + * @param array $settings Theme settings.
3083 + * @param array $properties Properties metadata.
3084 + * @param array $theme_json Theme JSON array.
3085 + * @param string $selector The style block selector.
2643 3086 * @param boolean $use_root_padding Whether to add custom properties at root level.
2644 - * @return array Returns the modified $declarations.
3087 + * @return array Returns the modified $declarations.
2645 3088 */
2646 3089 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2647 3090 if ( empty( $styles ) ) {
2648 3091 return array();
@@ -2766,10 +3209,10 @@
2766 3209 * @since 5.9.0 Added support for values of array type, which are returned as is.
2767 3210 * @since 6.1.0 Added the `$theme_json` parameter.
2768 3211 * @since 6.7.0 Added support for background image refs
2769 3212 *
2770 - * @param array $styles Styles subtree.
2771 - * @param array $path Which property to process.
3213 + * @param array $styles Styles subtree.
3214 + * @param array $path Which property to process.
2772 3215 * @param array $theme_json Theme JSON array.
2773 3216 * @return string|array Style property value.
2774 3217 */
2775 3218 protected static function get_property_value( $styles, $path, $theme_json = null ) {
@@ -3141,9 +3584,9 @@
3141 3584 * @since 6.1.0
3142 3585 *
3143 3586 * @param array $theme_json The theme.json converted to an array.
3144 3587 * @param array $selectors Optional list of selectors per block.
3145 - * @param array $options {
3588 + * @param array $options {
3146 3589 * Optional. An array of options for now used for internal purposes only (may change without notice).
3147 3590 *
3148 3591 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3149 3592 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
@@ -3156,10 +3599,11 @@
3156 3599 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3157 3600 return $nodes;
3158 3601 }
3159 3602
3160 - $include_variations = $options['include_block_style_variations'] ?? false;
3161 - $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3603 + $include_variations = $options['include_block_style_variations'] ?? false;
3604 + $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3605 + $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
3162 3606
3163 3607 // If only node paths are to be returned, skip selector assignment.
3164 3608 if ( ! $include_node_paths_only ) {
3165 3609 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
@@ -3224,14 +3668,14 @@
3224 3668
3225 3669 // Responsive block nodes: emit one node per breakpoint that has styles.
3226 3670 // These are rendered immediately after the base block node so that
3227 3671 // the cascade order is: .block{} → @media{.block{}}
3228 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3672 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3229 3673 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3230 3674 $nodes[] = array(
3231 3675 'name' => $name,
3232 3676 'path' => array( 'styles', 'blocks', $name, $breakpoint ),
3233 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3677 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3234 3678 'selector' => $selector,
3235 3679 'selectors' => $feature_selectors,
3236 3680 'elements' => $selectors[ $name ]['elements'] ?? array(),
3237 3681 'variations' => $variation_selectors,
@@ -3244,9 +3688,9 @@
3244 3688 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3245 3689 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3246 3690 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3247 3691 $has_responsive_pseudo = false;
3248 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3692 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3249 3693 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3250 3694 $has_responsive_pseudo = true;
3251 3695 break;
3252 3696 }
@@ -3289,14 +3733,14 @@
3289 3733
3290 3734 // Responsive pseudo nodes: emit one node per breakpoint that has
3291 3735 // this pseudo state, immediately after the default pseudo node.
3292 3736 // Cascade order: .block:hover{} → @media{.block:hover{}}
3293 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3737 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3294 3738 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3295 3739 $nodes[] = array(
3296 3740 'name' => $name,
3297 3741 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3298 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3742 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3299 3743 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3300 3744 'selectors' => $pseudo_feature_selectors,
3301 3745 'elements' => $selectors[ $name ]['elements'] ?? array(),
3302 3746 'variations' => $variation_selectors,
@@ -3306,9 +3750,9 @@
3306 3750 }
3307 3751 }
3308 3752 }
3309 3753
3310 - // Handle custom states (e.g. '@current' for navigation).
3754 + // Handle custom states (e.g. '-current' for navigation).
3311 3755 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3312 3756 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3313 3757 if (
3314 3758 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
@@ -3347,33 +3791,56 @@
3347 3791 }
3348 3792 }
3349 3793 }
3350 3794 }
3351 - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3352 - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3795 + /*
3796 + * Elements can be styled outside any breakpoint, inside one, or both,
3797 + * so collect the names from all of those places before looping. An
3798 + * element styled only inside a breakpoint still needs a node.
3799 + */
3800 + $block_node = $theme_json['styles']['blocks'][ $name ] ?? array();
3801 + $element_names = array_keys( $block_node['elements'] ?? array() );
3802 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3803 + $element_names = array_merge(
3804 + $element_names,
3805 + array_keys( $block_node[ $breakpoint ]['elements'] ?? array() )
3806 + );
3807 + }
3808 + $element_names = array_unique( $element_names );
3809 +
3810 + if ( ! empty( $element_names ) ) {
3811 + foreach ( $element_names as $element ) {
3353 3812 $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3354 3813 if ( $include_node_paths_only ) {
3355 - $nodes[] = array(
3356 - 'path' => $element_path,
3357 - );
3814 + if ( isset( $block_node['elements'][ $element ] ) ) {
3815 + $nodes[] = array(
3816 + 'path' => $element_path,
3817 + );
3818 + }
3358 3819 continue;
3359 3820 }
3360 3821
3822 + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3823 + continue;
3824 + }
3825 +
3361 3826 $element_selector = $selectors[ $name ]['elements'][ $element ];
3362 3827
3363 - $nodes[] = array(
3364 - 'path' => $element_path,
3365 - 'selector' => $element_selector,
3366 - );
3828 + if ( isset( $block_node['elements'][ $element ] ) ) {
3829 + $nodes[] = array(
3830 + 'path' => $element_path,
3831 + 'selector' => $element_selector,
3832 + );
3833 + }
3367 3834
3368 3835 // Responsive element nodes: one node per breakpoint that has
3369 3836 // styles for this element. Cascade: a{} → @media{a{}}
3370 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3837 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3371 3838 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3372 3839 $nodes[] = array(
3373 3840 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3374 3841 'selector' => $element_selector,
3375 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3842 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3376 3843 );
3377 3844 }
3378 3845 }
3379 3846
@@ -3379,45 +3846,29 @@
3379 3846
3380 3847 // Handle any pseudo selectors for the element.
3381 3848 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3382 3849 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3383 - // Create element pseudo node if default or any responsive breakpoint has the pseudo.
3384 - $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] );
3385 - if ( ! $has_element_pseudo ) {
3386 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) {
3387 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3388 - $has_element_pseudo = true;
3389 - break;
3390 - }
3391 - }
3850 + // Emit the default pseudo node only when the default state styles
3851 + // the pseudo. Otherwise get_styles_for_block() falls back to the
3852 + // element's base styles, outputting a rule the theme never defined.
3853 + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3854 + $nodes[] = array(
3855 + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
3856 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3857 + );
3392 3858 }
3393 3859
3394 - if ( $has_element_pseudo ) {
3395 - $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element );
3396 - if ( $include_node_paths_only ) {
3860 + // Responsive element pseudo nodes: one node per breakpoint
3861 + // that has this pseudo state for this element.
3862 + // Cascade: a:hover{} → @media{a:hover{}}
3863 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3864 + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3397 3865 $nodes[] = array(
3398 - 'path' => $element_pseudo_path,
3866 + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3867 + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3868 + 'media_query' => $responsive_media_queries[ $breakpoint ],
3399 3869 );
3400 - continue;
3401 3870 }
3402 -
3403 - $nodes[] = array(
3404 - 'path' => $element_pseudo_path,
3405 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3406 - );
3407 -
3408 - // Responsive element pseudo nodes: one node per breakpoint
3409 - // that has this pseudo state for this element.
3410 - // Cascade: a:hover{} → @media{a:hover{}}
3411 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3412 - if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3413 - $nodes[] = array(
3414 - 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3415 - 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3416 - 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ],
3417 - );
3418 - }
3419 - }
3420 3871 }
3421 3872 }
3422 3873 }
3423 3874 }
@@ -3437,14 +3888,15 @@
3437 3888 *
3438 3889 * @return string Styles for the block.
3439 3890 */
3440 3891 public function get_styles_for_block( $block_metadata ) {
3441 - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3442 - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3443 - $selector = $block_metadata['selector'];
3444 - $settings = $this->theme_json['settings'] ?? null;
3445 - $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3446 - $media_query = $block_metadata['media_query'] ?? null;
3892 + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3893 + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3894 + $selector = $block_metadata['selector'];
3895 + $settings = $this->theme_json['settings'] ?? null;
3896 + $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3897 + $media_query = $block_metadata['media_query'] ?? null;
3898 + $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null );
3447 3899
3448 3900 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3449 3901
3450 3902 // Update text indent selector for paragraph blocks based on the textIndent setting.
@@ -3499,10 +3951,20 @@
3499 3951 // Store variation metadata and node for layout styles generation.
3500 3952 // Only store if the variation has blockGap defined.
3501 3953 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3502 3954 // Append block selector to the variation selector for proper targeting.
3503 - $variation_metadata_with_selector = $style_variation;
3504 - $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3955 + $variation_metadata_with_selector = $style_variation;
3956 + $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3957 +
3958 + /*
3959 + * `get_layout_styles()` reads `name` as a block name, to check that the block
3960 + * supports layout at all. A variation node's `name` is the variation slug,
3961 + * which is never a registered block, so the check fails and every variation
3962 + * gap rule is discarded. Pass the block the variation belongs to, so the
3963 + * support check answers the question it is actually asking.
3964 + */
3965 + $variation_metadata_with_selector['name'] = $block_name;
3966 +
3505 3967 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3506 3968 'metadata' => $variation_metadata_with_selector,
3507 3969 'node' => $style_variation_node,
3508 3970 );
@@ -3512,15 +3974,15 @@
3512 3974 // This includes both base properties and feature-level selectors.
3513 3975 $variation_responsive_css = '';
3514 3976 $variation_responsive_pseudo_css = '';
3515 3977
3516 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
3978 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3517 3979 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3518 3980 continue;
3519 3981 }
3520 3982
3521 3983 $breakpoint_node = $style_variation_node[ $breakpoint ];
3522 - $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ];
3984 + $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3523 3985 // Process feature-level declarations for this breakpoint.
3524 3986 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3525 3987 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3526 3988 $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
@@ -3556,9 +4018,13 @@
3556 4018 // Process blockGap responsive layout styles for this variation.
3557 4019 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
3558 4020 $variation_layout_metadata = $style_variation;
3559 4021 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
3560 - $variation_responsive_css .= $this->get_layout_styles(
4022 +
4023 + // The variation slug is not a block name here either. See above.
4024 + $variation_layout_metadata['name'] = $block_name;
4025 +
4026 + $variation_responsive_css .= $this->get_layout_styles(
3561 4027 $variation_layout_metadata,
3562 4028 array(
3563 4029 'node' => $breakpoint_node,
3564 4030 'media_query' => $breakpoint_media,
@@ -4221,9 +4687,9 @@
4221 4687 * Gets a `default`'s preset name by a provided slug.
4222 4688 *
4223 4689 * @since 5.9.0
4224 4690 *
4225 - * @param string $slug The slug we want to find a match from default presets.
4691 + * @param string $slug The slug we want to find a match from default presets.
4226 4692 * @param array $base_path The path to inspect. It's 'settings' by default.
4227 4693 * @return string|null
4228 4694 */
4229 4695 protected function get_name_from_defaults( $slug, $base_path ) {
@@ -4271,10 +4737,10 @@
4271 4737 * @since 5.9.0
4272 4738 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
4273 4739 *
4274 4740 * @param array $theme_json Structure to sanitize.
4275 - * @param string $origin Optional. What source of data this object represents.
4276 - * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
4741 + * @param string $origin Optional. What source of data this object represents.
4742 + * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
4277 4743 * @return array Sanitized structure.
4278 4744 */
4279 4745 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4280 4746 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
@@ -4283,8 +4749,11 @@
4283 4749
4284 4750 $sanitized = array();
4285 4751
4286 4752 $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
4753 + if ( isset( $theme_json['styles'] ) ) {
4754 + $theme_json['styles'] = gutenberg_resolve_style_state_aliases( $theme_json['styles'] );
4755 + }
4287 4756
4288 4757 $blocks_metadata = static::get_blocks_metadata();
4289 4758 $valid_block_names = array_keys( $blocks_metadata );
4290 4759 $valid_element_names = array_keys( static::ELEMENTS );
@@ -4291,11 +4760,12 @@
4291 4760 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
4292 4761
4293 4762 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4294 4763
4295 - $blocks_metadata = static::get_blocks_metadata();
4296 - $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4297 - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4764 + $blocks_metadata = static::get_blocks_metadata();
4765 + $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
4766 + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4767 + $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
4298 4768
4299 4769 foreach ( $style_nodes as $metadata ) {
4300 4770 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4301 4771 if ( empty( $input ) ) {
@@ -4331,18 +4801,18 @@
4331 4801 }
4332 4802 }
4333 4803
4334 4804 // Re-add and process responsive breakpoint styles.
4335 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4805 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4336 4806 if ( isset( $input[ $breakpoint ] ) ) {
4337 4807 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4338 4808
4339 4809 if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4340 - $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] );
4810 + $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4341 4811 }
4342 4812
4343 4813 if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4344 - $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] );
4814 + $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4345 4815 }
4346 4816
4347 4817 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4348 4818 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4372,26 +4842,26 @@
4372 4842
4373 4843 $variation_output = static::remove_insecure_styles( $variation_input );
4374 4844
4375 4845 if ( isset( $variation_input['blocks'] ) ) {
4376 - $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] );
4846 + $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'], $responsive_media_queries );
4377 4847 }
4378 4848
4379 4849 if ( isset( $variation_input['elements'] ) ) {
4380 - $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] );
4850 + $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'], $responsive_media_queries );
4381 4851 }
4382 4852
4383 4853 // Re-add and process responsive breakpoint styles for variations.
4384 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4854 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4385 4855 if ( isset( $variation_input[ $breakpoint ] ) ) {
4386 4856 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4387 4857
4388 4858 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4389 - $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] );
4859 + $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4390 4860 }
4391 4861
4392 4862 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4393 - $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] );
4863 + $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4394 4864 }
4395 4865
4396 4866 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4397 4867 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
@@ -4421,9 +4891,9 @@
4421 4891 if ( empty( $input ) ) {
4422 4892 continue;
4423 4893 }
4424 4894
4425 - $output = static::remove_insecure_settings( $input );
4895 + $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4426 4896 if ( ! empty( $output ) ) {
4427 4897 _wp_array_set( $sanitized, $metadata['path'], $output );
4428 4898 }
4429 4899 }
@@ -4445,14 +4915,19 @@
4445 4915
4446 4916 /**
4447 4917 * Remove insecure element styles within a variation or block.
4448 4918 *
4919 + * When responsive media queries are provided, nested responsive state styles
4920 + * for those media-query keys are re-added after the base sanitization pass.
4921 + *
4449 4922 * @since 6.8.0
4450 4923 *
4451 - * @param array $elements The elements to process.
4924 + * @param array $elements The elements to process.
4925 + * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4926 + * viewport states. Default null.
4452 4927 * @return array The sanitized elements styles.
4453 4928 */
4454 - protected static function remove_insecure_element_styles( $elements ) {
4929 + protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4455 4930 $sanitized = array();
4456 4931 $valid_element_names = array_keys( static::ELEMENTS );
4457 4932
4458 4933 foreach ( $valid_element_names as $element_name ) {
@@ -4467,17 +4942,19 @@
4467 4942 }
4468 4943 }
4469 4944 }
4470 4945
4471 - // Re-add and process responsive breakpoint styles for elements.
4472 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4473 - if ( isset( $element_input[ $breakpoint ] ) ) {
4474 - $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4946 + if ( null !== $responsive_media_queries ) {
4947 + // Re-add and process responsive breakpoint styles for elements.
4948 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4949 + if ( isset( $element_input[ $breakpoint ] ) ) {
4950 + $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4475 4951
4476 - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4477 - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4478 - if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4479 - $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4952 + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4953 + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4954 + if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4955 + $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4956 + }
4480 4957 }
4481 4958 }
4482 4959 }
4483 4960 }
@@ -4491,31 +4968,38 @@
4491 4968
4492 4969 /**
4493 4970 * Remove insecure styles from inner blocks and their elements.
4494 4971 *
4972 + * When responsive media queries are provided, nested responsive state styles
4973 + * for those media-query keys are re-added after the base sanitization pass.
4974 + *
4495 4975 * @since 6.8.0
4496 4976 *
4497 - * @param array $blocks The block styles to process.
4977 + * @param array $blocks The block styles to process.
4978 + * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4979 + * viewport states. Default null.
4498 4980 * @return array Sanitized block type styles.
4499 4981 */
4500 - protected static function remove_insecure_inner_block_styles( $blocks ) {
4982 + protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4501 4983 $sanitized = array();
4502 4984 foreach ( $blocks as $block_type => $block_input ) {
4503 4985 $block_output = static::remove_insecure_styles( $block_input );
4504 4986
4505 4987 if ( isset( $block_input['elements'] ) ) {
4506 - $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] );
4988 + $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'], $responsive_media_queries );
4507 4989 }
4508 4990
4509 - // Re-add and process responsive breakpoint styles for inner blocks.
4510 - foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
4511 - if ( isset( $block_input[ $breakpoint ] ) ) {
4512 - $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4991 + if ( null !== $responsive_media_queries ) {
4992 + // Re-add and process responsive breakpoint styles for inner blocks.
4993 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4994 + if ( isset( $block_input[ $breakpoint ] ) ) {
4995 + $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4513 4996
4514 - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4515 - foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4516 - if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4517 - $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
4997 + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4998 + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4999 + if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
5000 + $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
5001 + }
4518 5002 }
4519 5003 }
4520 5004 }
4521 5005 }
@@ -4560,12 +5044,14 @@
4560 5044 * without the insecure settings.
4561 5045 *
4562 5046 * @since 5.9.0
4563 5047 *
4564 - * @param array $input Node to process.
5048 + * @param array $input Node to process.
5049 + * @param bool $allow_viewport Whether to preserve and sanitize top-level
5050 + * viewport settings.
4565 5051 * @return array
4566 5052 */
4567 - protected static function remove_insecure_settings( $input ) {
5053 + protected static function remove_insecure_settings( $input, $allow_viewport = true ) {
4568 5054 $output = array();
4569 5055 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4570 5056 foreach ( static::VALID_ORIGINS as $origin ) {
4571 5057 $path_with_origin = $preset_metadata['path'];
@@ -4616,8 +5102,12 @@
4616 5102
4617 5103 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4618 5104 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4619 5105
5106 + if ( $allow_viewport && array_key_exists( 'viewport', $input ) ) {
5107 + $output['viewport'] = static::sanitize_viewport_settings( $input['viewport'] );
5108 + }
5109 +
4620 5110 return $output;
4621 5111 }
4622 5112
4623 5113 /**
@@ -5294,8 +5784,9 @@
5294 5784 * This is used to convert the internal representation of variables to the CSS representation.
5295 5785 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
5296 5786 *
5297 5787 * @since 6.3.0
5788 + * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
5298 5789 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
5299 5790 * @return string The converted variable.
5300 5791 */
5301 5792 private static function convert_custom_properties( $value ) {
@@ -5302,15 +5793,32 @@
5302 5793 $prefix = 'var:';
5303 5794 $prefix_len = strlen( $prefix );
5304 5795 $token_in = '|';
5305 5796 $token_out = '--';
5306 - if ( 0 === strpos( $value, $prefix ) ) {
5307 - $unwrapped_name = str_replace(
5308 - $token_in,
5309 - $token_out,
5310 - substr( $value, $prefix_len )
5311 - );
5312 - $value = "var(--wp--$unwrapped_name)";
5797 + if ( str_starts_with( $value, $prefix ) ) {
5798 + $parts = explode( $token_in, substr( $value, $prefix_len ) );
5799 +
5800 + /*
5801 + * The slug of a preset reference is kebab-cased so the resulting
5802 + * custom property matches the one generated from the preset,
5803 + * whose slug is also kebab-cased (see `get_settings_values_by_slug()`).
5804 + * For slugs that are not already kebab-cased (e.g. `n27`), a verbatim
5805 + * conversion produces a reference to a custom property that does
5806 + * not exist (`--wp--preset--font-family--n27` instead of the
5807 + * generated `--wp--preset--font-family--n-27`).
5808 + *
5809 + * Duotone is the exception: its custom properties are generated by
5810 + * `WP_Duotone_Gutenberg` from the presets it registers in
5811 + * `get_all_global_styles_presets()`. Duotone references are
5812 + * kebab-cased all the same: the editor and the JS style engine
5813 + * kebab-case the references of every preset type, and
5814 + * `WP_Duotone_Gutenberg` looks up presets by kebab-cased filter ID.
5815 + */
5816 + if ( 3 === count( $parts ) && 'preset' === $parts[0] ) {
5817 + $parts[2] = _wp_to_kebab_case( $parts[2] );
5818 + }
5819 +
5820 + $value = 'var(--wp--' . implode( $token_out, $parts ) . ')';
5313 5821 }
5314 5822
5315 5823 return $value;
5316 5824 }
@@ -5319,9 +5827,9 @@
5319 5827 * Given a tree, converts the internal representation of variables to the CSS representation.
5320 5828 * It is recursive and modifies the input in-place.
5321 5829 *
5322 5830 * @since 6.3.0
5323 - * @param array $tree Input to process.
5831 + * @param array $tree Input to process.
5324 5832 * @return array The modified $tree.
5325 5833 */
5326 5834 private static function resolve_custom_css_format( $tree ) {
5327 5835 $prefix = 'var:';
@@ -5326,9 +5834,9 @@
5326 5834 private static function resolve_custom_css_format( $tree ) {
5327 5835 $prefix = 'var:';
5328 5836
5329 5837 foreach ( $tree as $key => $data ) {
5330 - if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
5838 + if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
5331 5839 $tree[ $key ] = self::convert_custom_properties( $data );
5332 5840 } elseif ( is_array( $data ) ) {
5333 5841 $tree[ $key ] = self::resolve_custom_css_format( $data );
5334 5842 }
@@ -5434,13 +5942,24 @@
5434 5942 $limit = 1;
5435 5943 $selector_parts = static::split_selector_list( $block_selector );
5436 5944 $result = array();
5437 5945
5946 + /*
5947 + * Append the variation class to each selector's ancestor: the first
5948 + * run of characters before any combinator (whitespace) or pseudo-class
5949 + * (`:`). Only the first match is replaced.
5950 + *
5951 + * Examples ("custom" variation):
5952 + * - `.wp-block` => `.wp-block.is-style-custom`
5953 + * - `.wp-block .inner` => `.wp-block.is-style-custom .inner`
5954 + * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)`
5955 + * - `:where(.outer .inner)` => `:where(.outer.is-style-custom .inner)`
5956 + */
5438 5957 foreach ( $selector_parts as $part ) {
5439 5958 $result[] = preg_replace_callback(
5440 - '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5959 + '/[^\s:]+/',
5441 5960 function ( $matches ) use ( $variation_class ) {
5442 - return $matches[1] . $matches[2] . $variation_class;
5961 + return $matches[0] . $variation_class;
5443 5962 },
5444 5963 $part,
5445 5964 $limit
5446 5965 );
@@ -5445,9 +5964,9 @@
5445 5964 $limit
5446 5965 );
5447 5966 }
5448 5967
5449 - return implode( ',', $result );
5968 + return implode( ', ', $result );
5450 5969 }
5451 5970
5452 5971 /**
5453 5972 * Applies a block style variation class to a feature selector.
@@ -5458,9 +5977,9 @@
5458 5977 * the variation class directly to the selector that will receive the
5459 5978 * declarations instead of deriving it by subtracting the root selector from
5460 5979 * the feature selector.
5461 5980 *
5462 - * @param array $style_variation Style variation metadata.
5981 + * @param array $style_variation Style variation metadata.
5463 5982 * @param string $feature_selector CSS selector for the feature.
5464 5983 * @return string Feature selector with block style variation selector added.
5465 5984 */
5466 5985 protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
@@ -5474,10 +5993,9 @@
5474 5993 $variation_class = ".is-style-$variation_name";
5475 5994 $selector_parts = static::split_selector_list( $feature_selector );
5476 5995 $selector_parts = array_map(
5477 5996 static function ( $selector ) use ( $variation_class ) {
5478 - $selector = trim( $selector );
5479 - $prefix = $variation_class . ' ';
5997 + $prefix = $variation_class . ' ';
5480 5998
5481 5999 if ( str_starts_with( $selector, $prefix ) ) {
5482 6000 return substr( $selector, strlen( $prefix ) );
5483 6001 }
@@ -5488,9 +6006,9 @@
5488 6006 );
5489 6007
5490 6008 return static::get_block_style_variation_selector(
5491 6009 $variation_name,
5492 - implode( ',', $selector_parts )
6010 + implode( ', ', $selector_parts )
5493 6011 );
5494 6012 }
5495 6013
5496 6014 /**