Make WordPress Core

Changeset 64238


Ignore:
Timestamp:
10/07/2026 10:53:31 PM (17 hours ago)
Author:
isabel_brison
Message:

Editor: normalize block gap values in layout and fix grid axial gap.

Ensures incorrect gap values don’t cause errors, fixes grid responsive column sizing when different vertical and horizontal gap values are present and ensures layouts that don’t support axial gap still get correct values.

Props isabel_brison, andrewserong.
Fixes #66263.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-supports/layout.php

    r63887 r64238  
    8181 * @since 7.1.0
    8282 *
    83  * @param string|array|null $gap_value Block gap value.
    84  * @return string|array|null Sanitized block gap value.
     83 * Numeric zero is converted to a string because it is valid CSS without a unit.
     84 * Other non-string values are rejected.
     85 *
     86 * @param mixed $gap_value Block gap value.
     87 * @return string|string[]|null Sanitized block gap value.
    8588 */
    8689function wp_sanitize_block_gap_value( $gap_value ) {
    8790        if ( is_array( $gap_value ) ) {
    8891                foreach ( $gap_value as $key => $value ) {
    89                         $gap_value[ $key ] = ! is_scalar( $value ) || ( $value && preg_match( '%[\\\(&=}]|/\*%', (string) $value ) ) ? null : $value;
    90                 }
    91 
    92                 return $gap_value;
     92                        $sanitized_value = wp_sanitize_block_gap_value( $value );
     93                        if ( ! is_string( $sanitized_value ) ) {
     94                                unset( $gap_value[ $key ] );
     95                                continue;
     96                        }
     97                        $gap_value[ $key ] = $sanitized_value;
     98                }
     99
     100                return empty( $gap_value ) ? null : $gap_value;
     101        }
     102
     103        if ( ( is_int( $gap_value ) || is_float( $gap_value ) ) && 0.0 === (float) $gap_value ) {
     104                return '0';
     105        }
     106
     107        if ( ! is_string( $gap_value ) ) {
     108                return null;
     109        }
     110
     111        if ( '' === trim( $gap_value ) ) {
     112                return null;
    93113        }
    94114
    … …  
    491511 * @access private
    492512 *
    493  * @param string               $selector                      CSS selector.
    494  * @param array                $layout                        Layout object. The one that is passed has already checked
    495  *                                                            the existence of default block layout.
    496  * @param bool                 $has_block_gap_support         Optional. Whether the theme has support for the block gap. Default false.
    497  * @param string|string[]|null $gap_value                     Optional. The block gap value to apply. Default null.
    498  * @param bool                 $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
    499  * @param string|array         $fallback_gap_value            Optional. The block gap value to apply. If it's an array expected properties are "top" and/or "left". Default '0.5em'.
    500  * @param array|null           $block_spacing                 Optional. Custom spacing set on the block. Default null.
    501  * @param array                $options                       {
     513 * @param string                         $selector                      CSS selector.
     514 * @param array                          $layout                        Layout object. The one that is passed has already checked
     515 *                                                                       the existence of default block layout.
     516 * @param bool                           $has_block_gap_support         Optional. Whether the theme has support for the block gap. Default false.
     517 * @param string|string[]|int|float|null $gap_value                     Optional. The block gap value to apply. Only zero is accepted as a
     518 *                                                                       numeric value. Default null.
     519 * @param bool                           $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the
     520 *                                                                       editor. Default false.
     521 * @param string|string[]|int|float|null $fallback_gap_value            Optional. The fallback block gap value to apply. Only zero is accepted
     522 *                                                                       as a numeric value. Default '0.5em'.
     523 * @param array|null                     $block_spacing                 Optional. Custom spacing set on the block. Default null.
     524 * @param array                          $options                       {
    502525 *     Optional. Extra options for internal callers. Default empty array.
    503526 *
    … …  
    510533 */
    511534function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null, $should_skip_gap_serialization = false, $fallback_gap_value = '0.5em', $block_spacing = null, $options = array() ) {
     535        // Normalize here as well as at external data boundaries because this function has direct callers.
     536        $gap_value          = wp_sanitize_block_gap_value( $gap_value );
     537        $fallback_gap_value = wp_sanitize_block_gap_value( $fallback_gap_value ) ?? '0.5em';
     538
    512539        $base_layout             = is_array( $layout ) ? $layout : array();
    513540        $viewport_overrides      = $options['viewport_overrides'] ?? null;
    … …  
    858885
    859886                /*
    860                  * If the gap value is an array, we use the "left" value because it represents the vertical gap, which
     887                 * If the gap value is an array, we use the "left" value because it represents the horizontal gap, which
    861888                 * is the relevant one for computation of responsive grid columns.
    862889                 */
    … …  
    887914                                        $process_value   = "var(--wp--preset--spacing--$slug)";
    888915                                }
     916                                if ( ! is_array( $gap_value ) || 'left' === $gap_side ) {
     917                                        $responsive_gap_value = $process_value;
     918                                }
    889919                                $combined_gap_value .= "$process_value ";
    890920                        }
    891                         $gap_value            = trim( $combined_gap_value );
    892                         $responsive_gap_value = $gap_value;
     921                        $gap_value = trim( $combined_gap_value );
    893922                }
    894923
    … …  
    11971226
    11981227                $gap_value          = wp_sanitize_block_gap_value( $style_attr['spacing']['blockGap'] ?? null );
    1199                 $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
     1228                $fallback_gap_value = wp_sanitize_block_gap_value(
     1229                        $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null
     1230                ) ?? '0.5em';
    12001231                $block_spacing      = $style_attr['spacing'] ?? null;
    12011232
    … …  
    12311262                }
    12321263
    1233                 $global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;
     1264                $global_block_gap_candidates = array(
     1265                        $variation_block_gap_value,
     1266                        $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? null,
     1267                        $global_styles['spacing']['blockGap'] ?? null,
     1268                );
     1269                $global_block_gap_value      = null;
     1270                foreach ( $global_block_gap_candidates as $candidate_gap_value ) {
     1271                        $candidate_gap_value = wp_sanitize_block_gap_value( $candidate_gap_value );
     1272                        if ( null !== $candidate_gap_value ) {
     1273                                $global_block_gap_value = $candidate_gap_value;
     1274                                break;
     1275                        }
     1276                }
    12341277
    12351278                if ( null !== $global_block_gap_value ) {
  • trunk/src/wp-includes/class-wp-theme-json.php

    r64093 r64238  
    23182318                 */
    23192319                if ( $has_block_gap_support || $has_fallback_gap_support ) {
    2320                         $block_gap_value = null;
     2320                        $block_gap_value     = null;
     2321                        $block_gap_row_value = null;
    23212322                        // Use a fallback gap value if block gap support is not available.
    23222323                        if ( ! $has_block_gap_support ) {
    … …  
    23282329                                $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
    23292330                        }
     2331                        $block_gap_row_value = $block_gap_value;
    23302332
    23312333                        // Support split row / column values and concatenate to a shorthand value.
    23322334                        if ( is_array( $block_gap_value ) ) {
    2333                                 if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
    2334                                         $gap_row         = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
    2335                                         $gap_column      = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
    2336                                         $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
     2335                                $has_block_gap_row_value    = isset( $block_gap_value['top'] );
     2336                                $has_block_gap_column_value = isset( $block_gap_value['left'] );
     2337
     2338                                if ( $has_block_gap_row_value || $has_block_gap_column_value ) {
     2339                                        $block_gap_row_value    = $has_block_gap_row_value
     2340                                                ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) )
     2341                                                : '0';
     2342                                        $block_gap_column_value = $has_block_gap_column_value
     2343                                                ? static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) )
     2344                                                : '0';
     2345                                        $block_gap_value        = $block_gap_row_value === $block_gap_column_value
     2346                                                ? $block_gap_row_value
     2347                                                : $block_gap_row_value . ' ' . $block_gap_column_value;
    23372348                                } else {
    2338                                         // Skip outputting gap value if not all sides are provided.
    2339                                         $block_gap_value = null;
     2349                                        // Skip outputting a gap value if neither supported axis is provided.
     2350                                        $block_gap_value     = null;
     2351                                        $block_gap_row_value = null;
    23402352                                }
    23412353                        }
    … …  
    23492361                                        }
    23502362
    2351                                         $class_name    = $layout_definition['className'] ?? false;
    2352                                         $spacing_rules = $layout_definition['spacingStyles'] ?? array();
     2363                                        $class_name       = $layout_definition['className'] ?? false;
     2364                                        $spacing_rules    = $layout_definition['spacingStyles'] ?? array();
     2365                                        $layout_gap_value = in_array( $layout_definition_key, array( 'default', 'constrained' ), true )
     2366                                                ? $block_gap_row_value
     2367                                                : $block_gap_value;
    23532368
    23542369                                        if (
    … …  
    23652380                                                                // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
    23662381                                                                foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
    2367                                                                         $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
     2382                                                                        $current_css_value = is_string( $css_value ) ? $css_value : $layout_gap_value;
    23682383                                                                        if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
    23692384                                                                                $declarations[] = array(
  • trunk/tests/phpunit/tests/block-supports/layout.php

    r64057 r64238  
    8585        /**
    8686         * @ticket 65667
     87         * @ticket 66263
     88         *
     89         * @dataProvider data_sanitize_block_gap_value
    8790         *
    8891         * @covers ::wp_sanitize_block_gap_value
    8992         */
    90         public function test_sanitize_block_gap_value_rejects_nested_array_values() {
    91                 $this->assertSame(
    92                         array(
    93                                 'top'  => null,
    94                                 'left' => '2rem',
    95                         ),
    96                         wp_sanitize_block_gap_value(
     93        public function test_sanitize_block_gap_value_normalizes_zero_and_rejects_other_non_string_values( $gap_value, $expected ) {
     94                $this->assertSame( $expected, wp_sanitize_block_gap_value( $gap_value ) );
     95        }
     96
     97        /**
     98         * Data provider for test_sanitize_block_gap_value_normalizes_zero_and_rejects_other_non_string_values().
     99         *
     100         * @return array[] Test data.
     101         */
     102        public function data_sanitize_block_gap_value() {
     103                return array(
     104                        'string value'           => array( '1rem', '1rem' ),
     105                        'empty string'           => array( '', null ),
     106                        'whitespace-only string' => array( " \t\n", null ),
     107                        'integer zero'           => array( 0, '0' ),
     108                        'floating-point zero'    => array( 0.0, '0' ),
     109                        'non-zero integer'       => array( 1, null ),
     110                        'boolean value'          => array( true, null ),
     111                        'object value'           => array( new stdClass(), null ),
     112                        'nested array value'     => array(
    97113                                array(
    98114                                        'top'  => array( '1rem' ),
    99115                                        'left' => '2rem',
    100                                 )
     116                                ),
     117                                array( 'left' => '2rem' ),
     118                        ),
     119                        'empty sanitized array'  => array( array( array( '1rem' ) ), null ),
     120                );
     121        }
     122
     123        /**
     124         * @ticket 66263
     125         *
     126         * @dataProvider data_wp_get_layout_style
     127         *
     128         * @covers ::wp_get_layout_style
     129         */
     130        public function test_wp_get_layout_style( $args, $expected_output ) {
     131                $this->assertSame(
     132                        $expected_output,
     133                        wp_get_layout_style(
     134                                $args['selector'],
     135                                $args['layout'],
     136                                $args['has_block_gap_support'],
     137                                $args['gap_value'],
     138                                false,
     139                                $args['fallback_gap_value'] ?? '0.5em'
    101140                        )
     141                );
     142        }
     143
     144        /**
     145         * Data provider for test_wp_get_layout_style().
     146         *
     147         * @return array[] Test data.
     148         */
     149        public function data_wp_get_layout_style() {
     150                return array(
     151                        'flex layout uses the default for malformed gap values' => array(
     152                                'args'            => array(
     153                                        'selector'              => '.wp-layout',
     154                                        'layout'                => array( 'type' => 'flex' ),
     155                                        'has_block_gap_support' => true,
     156                                        'gap_value'             => array( 'left' => '2rem' ),
     157                                        'fallback_gap_value'    => array(
     158                                                'top'  => array( '1rem' ),
     159                                                'left' => new stdClass(),
     160                                        ),
     161                                ),
     162                                'expected_output' => '.wp-layout{gap:0.5em 2rem;}',
     163                        ),
     164                        'flex layout ignores an empty block gap'    => array(
     165                                'args'            => array(
     166                                        'selector'              => '.wp-layout',
     167                                        'layout'                => array( 'type' => 'flex' ),
     168                                        'has_block_gap_support' => true,
     169                                        'gap_value'             => '',
     170                                ),
     171                                'expected_output' => '',
     172                        ),
     173                        'grid layout uses the default for malformed gap values' => array(
     174                                'args'            => array(
     175                                        'selector'              => '.wp-layout',
     176                                        'layout'                => array( 'type' => 'grid' ),
     177                                        'has_block_gap_support' => true,
     178                                        'gap_value'             => array( 'left' => '2rem' ),
     179                                        'fallback_gap_value'    => array(
     180                                                'top'  => array( '1rem' ),
     181                                                'left' => new stdClass(),
     182                                        ),
     183                                ),
     184                                'expected_output' => '.wp-layout{grid-template-columns:repeat(auto-fill, minmax(min(12rem, 100%), 1fr));container-type:inline-size;gap:0.5em 2rem;}',
     185                        ),
     186                        'grid layout uses horizontal gap for responsive columns' => array(
     187                                'args'            => array(
     188                                        'selector'              => '.wp-layout',
     189                                        'layout'                => array(
     190                                                'type'               => 'grid',
     191                                                'columnCount'        => 3,
     192                                                'minimumColumnWidth' => '12rem',
     193                                        ),
     194                                        'has_block_gap_support' => true,
     195                                        'gap_value'             => array(
     196                                                'top'  => '2rem',
     197                                                'left' => '3rem',
     198                                        ),
     199                                ),
     200                                'expected_output' => '.wp-layout{grid-template-columns:repeat(auto-fill, minmax(max(min(12rem, 100%), (100% - (3rem * (3 - 1))) /3), 1fr));container-type:inline-size;gap:2rem 3rem;}',
     201                        ),
     202                        'grid layout uses fallback when horizontal gap is missing' => array(
     203                                'args'            => array(
     204                                        'selector'              => '.wp-layout',
     205                                        'layout'                => array(
     206                                                'type'               => 'grid',
     207                                                'columnCount'        => 3,
     208                                                'minimumColumnWidth' => '12rem',
     209                                        ),
     210                                        'has_block_gap_support' => true,
     211                                        'gap_value'             => array( 'top' => '2rem' ),
     212                                        'fallback_gap_value'    => '1.2rem',
     213                                ),
     214                                'expected_output' => '.wp-layout{grid-template-columns:repeat(auto-fill, minmax(max(min(12rem, 100%), (100% - (1.2rem * (3 - 1))) /3), 1fr));container-type:inline-size;gap:2rem 1.2rem;}',
     215                        ),
     216                        'grid layout preserves zero horizontal gap' => array(
     217                                'args'            => array(
     218                                        'selector'              => '.wp-layout',
     219                                        'layout'                => array(
     220                                                'type'               => 'grid',
     221                                                'columnCount'        => 3,
     222                                                'minimumColumnWidth' => '12rem',
     223                                        ),
     224                                        'has_block_gap_support' => true,
     225                                        'gap_value'             => array(
     226                                                'top'  => '2rem',
     227                                                'left' => '0',
     228                                        ),
     229                                ),
     230                                'expected_output' => '.wp-layout{grid-template-columns:repeat(auto-fill, minmax(max(min(12rem, 100%), (100% - (0px * (3 - 1))) /3), 1fr));container-type:inline-size;gap:2rem 0;}',
     231                        ),
    102232                );
    103233        }
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r64104 r64238  
    20972097                        ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}',
    20982098                        $theme_json->get_stylesheet( array( 'styles' ) )
     2099                );
     2100        }
     2101
     2102        /**
     2103         * @ticket 66263
     2104         *
     2105         * @dataProvider data_get_stylesheet_with_axial_block_gap
     2106         *
     2107         * @param array  $block_gap           Block gap value.
     2108         * @param string $expected_row_gap    Expected row gap value.
     2109         * @param string $expected_column_gap Expected column gap value.
     2110         */
     2111        public function test_get_stylesheet_generates_layout_styles_with_axial_block_gap( $block_gap, $expected_row_gap, $expected_column_gap ) {
     2112                $theme_json = new WP_Theme_JSON(
     2113                        array(
     2114                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     2115                                'settings' => array(
     2116                                        'spacing' => array(
     2117                                                'blockGap' => true,
     2118                                        ),
     2119                                ),
     2120                                'styles'   => array(
     2121                                        'blocks' => array(
     2122                                                'core/group' => array(
     2123                                                        'spacing' => array(
     2124                                                                'blockGap' => $block_gap,
     2125                                                        ),
     2126                                                ),
     2127                                        ),
     2128                                ),
     2129                        ),
     2130                        'default'
     2131                );
     2132
     2133                $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
     2134
     2135                $this->assertMatchesRegularExpression( '/:where\(\.wp-block-group-is-layout-flow\) > \*\{margin-block-start:\s*' . preg_quote( $expected_row_gap, '/' ) . ';margin-block-end:\s*0;\}/', $stylesheet );
     2136                $this->assertMatchesRegularExpression( '/:where\(\.wp-block-group-is-layout-constrained\) > \*\{margin-block-start:\s*' . preg_quote( $expected_row_gap, '/' ) . ';margin-block-end:\s*0;\}/', $stylesheet );
     2137                $this->assertMatchesRegularExpression( '/:where\(\.wp-block-group-is-layout-flex\)\{gap:\s*' . preg_quote( $expected_row_gap, '/' ) . ' ' . preg_quote( $expected_column_gap, '/' ) . ';\}/', $stylesheet );
     2138                $this->assertMatchesRegularExpression( '/:where\(\.wp-block-group-is-layout-grid\)\{gap:\s*' . preg_quote( $expected_row_gap, '/' ) . ' ' . preg_quote( $expected_column_gap, '/' ) . ';\}/', $stylesheet );
     2139        }
     2140
     2141        /**
     2142         * Data provider for test_get_stylesheet_generates_layout_styles_with_axial_block_gap().
     2143         *
     2144         * @return array[] Test data.
     2145         */
     2146        public function data_get_stylesheet_with_axial_block_gap() {
     2147                return array(
     2148                        'different row and column gaps' => array(
     2149                                array(
     2150                                        'top'  => '1em',
     2151                                        'left' => '2em',
     2152                                ),
     2153                                '1em',
     2154                                '2em',
     2155                        ),
     2156                        'row gap only'                  => array(
     2157                                array( 'top' => '1em' ),
     2158                                '1em',
     2159                                '0',
     2160                        ),
     2161                        'column gap only'               => array(
     2162                                array( 'left' => '2em' ),
     2163                                '0',
     2164                                '2em',
     2165                        ),
    20992166                );
    21002167        }
Note: See TracChangeset for help on using the changeset viewer.