[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/block-supports/ -> layout.php (source)

   1  <?php
   2  /**
   3   * Layout block support flag.
   4   *
   5   * @package WordPress
   6   * @since 5.8.0
   7   */
   8  
   9  /**
  10   * Gets the first style variation name from a className string that matches a registered style.
  11   *
  12   * @since 7.0.0
  13   *
  14   * @param string                              $class_name        CSS class string for a block.
  15   * @param array<string, array<string, mixed>> $registered_styles Currently registered block styles.
  16   * @return string|null The name of the first registered variation, or null if none found.
  17   */
  18  function wp_get_block_style_variation_name_from_registered_style( string $class_name, array $registered_styles = array() ): ?string {
  19      if ( ! $class_name ) {
  20          return null;
  21      }
  22  
  23      $registered_names = array_filter( array_column( $registered_styles, 'name' ) );
  24  
  25      $prefix = 'is-style-';
  26      $length = strlen( $prefix );
  27  
  28      foreach ( explode( ' ', $class_name ) as $class ) {
  29          if ( str_starts_with( $class, $prefix ) ) {
  30              $variation = substr( $class, $length );
  31              if ( 'default' !== $variation && in_array( $variation, $registered_names, true ) ) {
  32                  return $variation;
  33              }
  34          }
  35      }
  36  
  37      return null;
  38  }
  39  
  40  /**
  41   * Returns the child-layout-only subset of a layout object.
  42   *
  43   * @since 7.1.0
  44   *
  45   * @param mixed $layout Layout object.
  46   * @return array Child layout values, or an empty array.
  47   */
  48  function wp_get_layout_child_values( $layout ) {
  49      if ( ! is_array( $layout ) ) {
  50          return array();
  51      }
  52  
  53      return array_intersect_key(
  54          $layout,
  55          array_flip( array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' ) )
  56      );
  57  }
  58  
  59  /**
  60   * Returns the container-layout subset of a layout object.
  61   *
  62   * @since 7.1.0
  63   *
  64   * @param mixed $layout Layout object.
  65   * @return array Container layout values, or an empty array.
  66   */
  67  function wp_get_layout_container_values( $layout ) {
  68      if ( ! is_array( $layout ) ) {
  69          return array();
  70      }
  71  
  72      return array_diff_key(
  73          $layout,
  74          array_flip( array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' ) )
  75      );
  76  }
  77  
  78  /**
  79   * Sanitizes a block gap value before layout style generation.
  80   *
  81   * @since 7.1.0
  82   *
  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.
  88   */
  89  function wp_sanitize_block_gap_value( $gap_value ) {
  90      if ( is_array( $gap_value ) ) {
  91          foreach ( $gap_value as $key => $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;
 113      }
 114  
 115      // Allow valid CSS custom property references, e.g. var(--wp--preset--spacing--32).
 116      if ( preg_match( '/^var\(--[a-zA-Z0-9_-]+\)$/', $gap_value ) ) {
 117          return $gap_value;
 118      }
 119  
 120      return $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
 121  }
 122  
 123  /**
 124   * Returns child layout styles for a block affected by its parent's layout.
 125   *
 126   * @since 7.1.0
 127   *
 128   * @param string     $selector           CSS selector.
 129   * @param array      $child_layout       Child layout values.
 130   * @param array      $parent_layout      Parent layout values.
 131   * @param array|null $viewport_overrides Optional. Child viewport layout overrides to emit.
 132   * @return array Child layout style rules.
 133   */
 134  function wp_get_child_layout_style_rules( $selector, $child_layout, $parent_layout = array(), $viewport_overrides = null ) {
 135      $base_child_layout              = is_array( $child_layout ) ? $child_layout : array();
 136      $viewport_overrides             = is_array( $viewport_overrides ) ? $viewport_overrides : null;
 137      $child_layout                   = null === $viewport_overrides ? $base_child_layout : array_replace( $base_child_layout, $viewport_overrides );
 138      $child_layout_declarations      = array();
 139      $child_layout_styles            = array();
 140      $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
 141          return array_key_exists( $property, $viewport_overrides );
 142      };
 143  
 144      $self_stretch      = $child_layout['selfStretch'] ?? null;
 145      $base_self_stretch = $base_child_layout['selfStretch'] ?? null;
 146  
 147      /*
 148       * These are the serialized `selfStretch` values. `max` used to be called
 149       * "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`.
 150       */
 151      $flex_child_layout_values = array(
 152          'fit'   => 'fit',
 153          'grow'  => 'fill',
 154          'max'   => 'fixed',
 155          'fixed' => 'fixedNoShrink',
 156      );
 157      $flex_size_values         = array(
 158          $flex_child_layout_values['max'],
 159          $flex_child_layout_values['fixed'],
 160      );
 161  
 162      if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( 'flexSize' ) ) {
 163          if (
 164              null !== $viewport_overrides &&
 165              ( $flex_child_layout_values['fit'] === $self_stretch || $flex_child_layout_values['grow'] === $self_stretch ) &&
 166              in_array( $base_self_stretch, $flex_size_values, true ) &&
 167              isset( $base_child_layout['flexSize'] )
 168          ) {
 169              $child_layout_declarations['flex-basis'] = 'unset';
 170              if ( $flex_child_layout_values['fixed'] === $base_self_stretch ) {
 171                  $child_layout_declarations['flex-shrink'] = 'unset';
 172              }
 173          }
 174          if ( in_array( $self_stretch, $flex_size_values, true ) && isset( $child_layout['flexSize'] ) ) {
 175              $child_layout_declarations['flex-basis'] = $child_layout['flexSize'];
 176              if ( $flex_child_layout_values['fixed'] === $self_stretch ) {
 177                  $child_layout_declarations['flex-shrink'] = '0';
 178              } elseif ( null !== $viewport_overrides && $flex_child_layout_values['fixed'] === $base_self_stretch ) {
 179                  $child_layout_declarations['flex-shrink'] = 'unset';
 180              }
 181              $child_layout_declarations['box-sizing'] = 'border-box';
 182          } elseif ( $flex_child_layout_values['grow'] === $self_stretch ) {
 183              $child_layout_declarations['flex-grow'] = '1';
 184          }
 185      }
 186  
 187      /*
 188       * Grid line numbers and spans are whole numbers. The editor stores them as numbers, but
 189       * content saved by WordPress 6.3 to 6.6 stored them as numeric strings, and that
 190       * migration only runs when a block is parsed in JavaScript, so the front end still sees
 191       * strings. Accept any numeric value and cast it, and treat anything else as absent
 192       * because it can't render as valid CSS.
 193       */
 194      $column_start_attr = $child_layout['columnStart'] ?? null;
 195      $column_start      = is_numeric( $column_start_attr ) ? (int) $column_start_attr : null;
 196      $column_span_attr  = $child_layout['columnSpan'] ?? null;
 197      $column_span       = is_numeric( $column_span_attr ) ? (int) $column_span_attr : null;
 198      if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
 199          if ( $column_start && $column_span ) {
 200              $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
 201          } elseif ( $column_start ) {
 202              $child_layout_declarations['grid-column'] = "$column_start";
 203          } elseif ( $column_span ) {
 204              $child_layout_declarations['grid-column'] = "span $column_span";
 205          }
 206      }
 207  
 208      $row_start_attr = $child_layout['rowStart'] ?? null;
 209      $row_start      = is_numeric( $row_start_attr ) ? (int) $row_start_attr : null;
 210      $row_span_attr  = $child_layout['rowSpan'] ?? null;
 211      $row_span       = is_numeric( $row_span_attr ) ? (int) $row_span_attr : null;
 212      if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
 213          if ( $row_start && $row_span ) {
 214              $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
 215          } elseif ( $row_start ) {
 216              $child_layout_declarations['grid-row'] = "$row_start";
 217          } elseif ( $row_span ) {
 218              $child_layout_declarations['grid-row'] = "span $row_span";
 219          }
 220      }
 221  
 222      if ( ! empty( $child_layout_declarations ) ) {
 223          $child_layout_styles[] = array(
 224              'selector'     => $selector,
 225              'declarations' => $child_layout_declarations,
 226          );
 227      }
 228  
 229      $minimum_column_width_attr = $parent_layout['minimumColumnWidth'] ?? null;
 230      $minimum_column_width      = is_string( $minimum_column_width_attr ) ? $minimum_column_width_attr : null;
 231      $column_count              = $parent_layout['columnCount'] ?? null;
 232  
 233      /*
 234       * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
 235       * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
 236       * once the grid has less columns than the start.
 237       * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
 238       * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
 239       */
 240      if ( null === $viewport_overrides && ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
 241          $column_span_number  = floatval( $column_span );
 242          $column_start_number = floatval( $column_start );
 243          $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
 244          $parent_column_value = floatval( $parent_column_width );
 245          $parent_column_unit  = explode( (string) $parent_column_value, $parent_column_width );
 246  
 247          $num_cols_to_break_at = 2;
 248          if ( $column_span_number && $column_start_number ) {
 249              $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
 250          } elseif ( $column_span_number ) {
 251              $num_cols_to_break_at = $column_span_number;
 252          } else {
 253              $num_cols_to_break_at = $column_start_number;
 254          }
 255  
 256          /*
 257           * If there is no unit, the width has somehow been mangled so we reset both unit and value
 258           * to defaults.
 259           * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
 260           */
 261          if ( count( $parent_column_unit ) <= 1 ) {
 262              $parent_column_unit  = 'rem';
 263              $parent_column_value = 12;
 264          } else {
 265              $parent_column_unit = $parent_column_unit[1];
 266  
 267              if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
 268                  $parent_column_unit = 'rem';
 269              }
 270          }
 271  
 272          /*
 273           * A default gap value is used for this computation because custom gap values may not be
 274           * viable to use in the computation of the container query value.
 275           */
 276          $default_gap_value             = 'px' === $parent_column_unit ? 24 : 1.5;
 277          $container_query_value         = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
 278          $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
 279          $container_query_value         = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
 280          // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
 281          $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
 282  
 283          $child_layout_styles[] = array(
 284              'rules_group'  => "@container (max-width: $container_query_value )",
 285              'selector'     => $selector,
 286              'declarations' => array(
 287                  'grid-column' => $grid_column_value,
 288                  'grid-row'    => 'auto',
 289              ),
 290          );
 291      }
 292  
 293      return $child_layout_styles;
 294  }
 295  
 296  /**
 297   * Returns layout definitions, keyed by layout type.
 298   *
 299   * Provides a common definition of slugs, classnames, base styles, and spacing styles for each layout type.
 300   * When making changes or additions to layout definitions, the corresponding JavaScript definitions should
 301   * also be updated.
 302   *
 303   * @since 6.3.0
 304   * @since 6.6.0 Updated specificity for compatibility with 0-1-0 global styles specificity.
 305   * @access private
 306   *
 307   * @return array[] Layout definitions.
 308   */
 309  function wp_get_layout_definitions() {
 310      $layout_definitions = array(
 311          'default'     => array(
 312              'name'          => 'default',
 313              'slug'          => 'flow',
 314              'className'     => 'is-layout-flow',
 315              'baseStyles'    => array(
 316                  array(
 317                      'selector' => ' > .alignleft',
 318                      'rules'    => array(
 319                          'float'        => 'left',
 320                          'margin-left'  => '0',
 321                          'margin-right' => '2em',
 322                      ),
 323                  ),
 324                  array(
 325                      'selector' => ' > .alignright',
 326                      'rules'    => array(
 327                          'float'        => 'right',
 328                          'margin-left'  => '2em',
 329                          'margin-right' => '0',
 330                      ),
 331                  ),
 332                  array(
 333                      'selector' => ' > .aligncenter',
 334                      'rules'    => array(
 335                          'margin-left'  => 'auto !important',
 336                          'margin-right' => 'auto !important',
 337                      ),
 338                  ),
 339              ),
 340              'spacingStyles' => array(
 341                  array(
 342                      'selector' => ' > :first-child',
 343                      'rules'    => array(
 344                          'margin-block-start' => '0',
 345                      ),
 346                  ),
 347                  array(
 348                      'selector' => ' > :last-child',
 349                      'rules'    => array(
 350                          'margin-block-end' => '0',
 351                      ),
 352                  ),
 353                  array(
 354                      'selector' => ' > *',
 355                      'rules'    => array(
 356                          'margin-block-start' => null,
 357                          'margin-block-end'   => '0',
 358                      ),
 359                  ),
 360              ),
 361          ),
 362          'constrained' => array(
 363              'name'          => 'constrained',
 364              'slug'          => 'constrained',
 365              'className'     => 'is-layout-constrained',
 366              'baseStyles'    => array(
 367                  array(
 368                      'selector' => ' > .alignleft',
 369                      'rules'    => array(
 370                          'float'        => 'left',
 371                          'margin-left'  => '0',
 372                          'margin-right' => '2em',
 373                      ),
 374                  ),
 375                  array(
 376                      'selector' => ' > .alignright',
 377                      'rules'    => array(
 378                          'float'        => 'right',
 379                          'margin-left'  => '2em',
 380                          'margin-right' => '0',
 381                      ),
 382                  ),
 383                  array(
 384                      'selector' => ' > .aligncenter',
 385                      'rules'    => array(
 386                          'margin-left'  => 'auto !important',
 387                          'margin-right' => 'auto !important',
 388                      ),
 389                  ),
 390                  array(
 391                      'selector' => ' > :where(:not(.alignleft):not(.alignright):not(.alignfull))',
 392                      'rules'    => array(
 393                          'max-width'    => 'var(--wp--style--global--content-size)',
 394                          'margin-left'  => 'auto !important',
 395                          'margin-right' => 'auto !important',
 396                      ),
 397                  ),
 398                  array(
 399                      'selector' => ' > .alignwide',
 400                      'rules'    => array(
 401                          'max-width' => 'var(--wp--style--global--wide-size)',
 402                      ),
 403                  ),
 404              ),
 405              'spacingStyles' => array(
 406                  array(
 407                      'selector' => ' > :first-child',
 408                      'rules'    => array(
 409                          'margin-block-start' => '0',
 410                      ),
 411                  ),
 412                  array(
 413                      'selector' => ' > :last-child',
 414                      'rules'    => array(
 415                          'margin-block-end' => '0',
 416                      ),
 417                  ),
 418                  array(
 419                      'selector' => ' > *',
 420                      'rules'    => array(
 421                          'margin-block-start' => null,
 422                          'margin-block-end'   => '0',
 423                      ),
 424                  ),
 425              ),
 426          ),
 427          'flex'        => array(
 428              'name'          => 'flex',
 429              'slug'          => 'flex',
 430              'className'     => 'is-layout-flex',
 431              'displayMode'   => 'flex',
 432              'baseStyles'    => array(
 433                  array(
 434                      'selector' => '',
 435                      'rules'    => array(
 436                          'flex-wrap'   => 'wrap',
 437                          'align-items' => 'center',
 438                      ),
 439                  ),
 440                  array(
 441                      'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
 442                      'rules'    => array(
 443                          'margin' => '0',
 444                      ),
 445                  ),
 446              ),
 447              'spacingStyles' => array(
 448                  array(
 449                      'selector' => '',
 450                      'rules'    => array(
 451                          'gap' => null,
 452                      ),
 453                  ),
 454              ),
 455          ),
 456          'grid'        => array(
 457              'name'          => 'grid',
 458              'slug'          => 'grid',
 459              'className'     => 'is-layout-grid',
 460              'displayMode'   => 'grid',
 461              'baseStyles'    => array(
 462                  array(
 463                      'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
 464                      'rules'    => array(
 465                          'margin' => '0',
 466                      ),
 467                  ),
 468              ),
 469              'spacingStyles' => array(
 470                  array(
 471                      'selector' => '',
 472                      'rules'    => array(
 473                          'gap' => null,
 474                      ),
 475                  ),
 476              ),
 477          ),
 478      );
 479  
 480      return $layout_definitions;
 481  }
 482  
 483  /**
 484   * Registers the layout block attribute for block types that support it.
 485   *
 486   * @since 5.8.0
 487   * @since 6.3.0 Check for layout support via the `layout` key with fallback to `__experimentalLayout`.
 488   * @access private
 489   *
 490   * @param WP_Block_Type $block_type Block Type.
 491   */
 492  function wp_register_layout_support( $block_type ) {
 493      $support_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false );
 494      if ( $support_layout ) {
 495          if ( ! $block_type->attributes ) {
 496              $block_type->attributes = array();
 497          }
 498  
 499          if ( ! array_key_exists( 'layout', $block_type->attributes ) ) {
 500              $block_type->attributes['layout'] = array(
 501                  'type' => 'object',
 502              );
 503          }
 504      }
 505  }
 506  
 507  /**
 508   * Generates the CSS corresponding to the provided layout.
 509   *
 510   * @since 5.9.0
 511   * @since 6.1.0 Added `$block_spacing` param, use style engine to enqueue styles.
 512   * @since 6.3.0 Added grid layout type.
 513   * @since 6.6.0 Removed duplicated selector from layout styles.
 514   *              Enabled negative margins for alignfull children of blocks with custom padding.
 515   * @since 7.1.0 Added options array with options to process responsive styles.
 516   * @access private
 517   *
 518   * @param string                         $selector                      CSS selector.
 519   * @param array                          $layout                        Layout object. The one that is passed has already checked
 520   *                                                                       the existence of default block layout.
 521   * @param bool                           $has_block_gap_support         Optional. Whether the theme has support for the block gap. Default false.
 522   * @param string|string[]|int|float|null $gap_value                     Optional. The block gap value to apply. Only zero is accepted as a
 523   *                                                                       numeric value. Default null.
 524   * @param bool                           $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the
 525   *                                                                       editor. Default false.
 526   * @param string|string[]|int|float|null $fallback_gap_value            Optional. The fallback block gap value to apply. Only zero is accepted
 527   *                                                                       as a numeric value. Default '0.5em'.
 528   * @param array|null                     $block_spacing                 Optional. Custom spacing set on the block. Default null.
 529   * @param array                          $options                       {
 530   *     Optional. Extra options for internal callers. Default empty array.
 531   *
 532   *     @type array       $viewport_overrides     An array of layout property overrides for the sake of style generation,
 533   *                                               keyed by property name.
 534   *     @type string|null $rules_group            Optional group name for the rules. Default null.
 535   *     @type bool        $has_block_gap_override Whether the block gap has been overridden. Default false.
 536   * }
 537   * @return string CSS styles on success. Else, empty string.
 538   */
 539  function 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() ) {
 540      // Normalize here as well as at external data boundaries because this function has direct callers.
 541      $gap_value          = wp_sanitize_block_gap_value( $gap_value );
 542      $fallback_gap_value = wp_sanitize_block_gap_value( $fallback_gap_value ) ?? '0.5em';
 543  
 544      $base_layout             = is_array( $layout ) ? $layout : array();
 545      $viewport_overrides      = $options['viewport_overrides'] ?? null;
 546      $layout_for_styles       = null === $viewport_overrides ? $base_layout : array_replace( $base_layout, $viewport_overrides );
 547      $layout_type             = $base_layout['type'] ?? 'default';
 548      $rules_group             = $options['rules_group'] ?? null;
 549      $has_block_gap_override  = ! empty( $options['has_block_gap_override'] );
 550      $should_output_block_gap = null === $viewport_overrides || $has_block_gap_override;
 551  
 552      /*
 553       * Viewport styles only store changed fields. If a field is present with null,
 554       * the user cleared a value inherited from the default viewport, so check
 555       * whether the key exists rather than whether the value is truthy.
 556       */
 557      $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
 558          return array_key_exists( $property, $viewport_overrides );
 559      };
 560      $layout_styles                  = array();
 561  
 562      if ( 'default' === $layout_type ) {
 563          if ( $has_block_gap_support && $should_output_block_gap ) {
 564              if ( is_array( $gap_value ) ) {
 565                  $gap_value = $gap_value['top'] ?? null;
 566              }
 567              if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
 568                  // Get spacing CSS variable from preset value if provided.
 569                  if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
 570                      $index_to_splice = strrpos( $gap_value, '|' ) + 1;
 571                      $slug            = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
 572                      $gap_value       = "var(--wp--preset--spacing--$slug)";
 573                  }
 574  
 575                  array_push(
 576                      $layout_styles,
 577                      array(
 578                          'selector'     => "$selector > *",
 579                          'declarations' => array(
 580                              'margin-block-start' => '0',
 581                              'margin-block-end'   => '0',
 582                          ),
 583                      ),
 584                      array(
 585                          'selector'     => "$selector > * + *",
 586                          'declarations' => array(
 587                              'margin-block-start' => $gap_value,
 588                              'margin-block-end'   => '0',
 589                          ),
 590                      )
 591                  );
 592              }
 593          }
 594      } elseif ( 'constrained' === $layout_type ) {
 595          // The schemas and editor UI only produce strings here, so treat a non-string
 596          // value as absent rather than casting it — it couldn't render as valid CSS anyway.
 597          $content_size_attr    = $layout_for_styles['contentSize'] ?? null;
 598          $content_size         = is_string( $content_size_attr ) ? $content_size_attr : '';
 599          $wide_size_attr       = $layout_for_styles['wideSize'] ?? null;
 600          $wide_size            = is_string( $wide_size_attr ) ? $wide_size_attr : '';
 601          $justify_content_attr = $layout_for_styles['justifyContent'] ?? null;
 602          $justify_content      = is_string( $justify_content_attr ) ? $justify_content_attr : 'center';
 603  
 604          // Check if viewport-specific ("override") values exist. Null values are valid and mean the user cleared a value inherited from the default viewport.
 605          $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
 606          $has_content_size_override    = null !== $viewport_overrides && $has_viewport_property_override( 'contentSize' );
 607          $has_wide_size_override       = null !== $viewport_overrides && $has_viewport_property_override( 'wideSize' );
 608  
 609          /*
 610           * Styles should be output either if there are no viewport overrides (this is the default case), or if the user has set a new viewport-specific
 611           * value for contentSize or wideSize. If a viewport clears a custom constrained size, reset to the global layout variable.
 612           */
 613          $should_output_constrained_sizes = null === $viewport_overrides || $has_content_size_override || $has_wide_size_override;
 614          $is_resetting_constrained_sizes  = null !== $viewport_overrides &&
 615              (
 616                  ( $has_content_size_override && ! $content_size ) ||
 617                  ( $has_wide_size_override && ! $wide_size )
 618              );
 619  
 620          // If a viewport clears a custom constrained size, reset to the global layout variable.
 621          $all_max_width_value  = $content_size
 622              ? $content_size
 623              : ( $wide_size && ! $has_content_size_override ? $wide_size : 'var(--wp--style--global--content-size, none)' );
 624          $wide_max_width_value = $wide_size
 625              ? $wide_size
 626              : ( $content_size && ! $has_wide_size_override ? $content_size : 'var(--wp--style--global--wide-size, none)' );
 627  
 628          // Make sure there is a single CSS rule, and all tags are stripped for security.
 629          $all_max_width_value  = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
 630          $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
 631  
 632          $margin_left  = 'left' === $justify_content ? '0 !important' : 'auto !important';
 633          $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
 634  
 635          if ( $should_output_constrained_sizes && ( $content_size || $wide_size || $is_resetting_constrained_sizes ) ) {
 636              $content_size_declarations = array(
 637                  'max-width' => $all_max_width_value,
 638              );
 639  
 640              if ( null === $viewport_overrides || $has_justify_content_override ) {
 641                  $content_size_declarations['margin-left']  = $margin_left;
 642                  $content_size_declarations['margin-right'] = $margin_right;
 643              }
 644  
 645              array_push(
 646                  $layout_styles,
 647                  array(
 648                      'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
 649                      'declarations' => $content_size_declarations,
 650                  ),
 651                  array(
 652                      'selector'     => "$selector > .alignwide",
 653                      'declarations' => array( 'max-width' => $wide_max_width_value ),
 654                  ),
 655                  array(
 656                      'selector'     => "$selector .alignfull",
 657                      'declarations' => array( 'max-width' => 'none' ),
 658                  )
 659              );
 660          }
 661  
 662          if ( null === $viewport_overrides && isset( $block_spacing ) ) {
 663              $block_spacing_values = wp_style_engine_get_styles(
 664                  array(
 665                      'spacing' => $block_spacing,
 666                  )
 667              );
 668  
 669              /*
 670               * Handle negative margins for alignfull children of blocks with custom padding set.
 671               * They're added separately because padding might only be set on one side.
 672               */
 673              if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
 674                  $padding_right = $block_spacing_values['declarations']['padding-right'];
 675                  // Add unit if 0.
 676                  if ( '0' === $padding_right ) {
 677                      $padding_right = '0px';
 678                  }
 679                  $layout_styles[] = array(
 680                      'selector'     => "$selector > .alignfull",
 681                      'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
 682                  );
 683              }
 684              if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
 685                  $padding_left = $block_spacing_values['declarations']['padding-left'];
 686                  // Add unit if 0.
 687                  if ( '0' === $padding_left ) {
 688                      $padding_left = '0px';
 689                  }
 690                  $layout_styles[] = array(
 691                      'selector'     => "$selector > .alignfull",
 692                      'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
 693                  );
 694              }
 695          }
 696  
 697          if ( $has_justify_content_override && ! $should_output_constrained_sizes ) {
 698              $layout_styles[] = array(
 699                  'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
 700                  'declarations' => array(
 701                      'margin-left'  => $margin_left,
 702                      'margin-right' => $margin_right,
 703                  ),
 704              );
 705          } elseif ( null === $viewport_overrides ) {
 706              if ( 'left' === $justify_content ) {
 707                  $layout_styles[] = array(
 708                      'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
 709                      'declarations' => array( 'margin-left' => '0 !important' ),
 710                  );
 711              }
 712  
 713              if ( 'right' === $justify_content ) {
 714                  $layout_styles[] = array(
 715                      'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
 716                      'declarations' => array( 'margin-right' => '0 !important' ),
 717                  );
 718              }
 719          }
 720  
 721          if ( $has_block_gap_support && $should_output_block_gap ) {
 722              if ( is_array( $gap_value ) ) {
 723                  $gap_value = $gap_value['top'] ?? null;
 724              }
 725              if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
 726                  // Get spacing CSS variable from preset value if provided.
 727                  if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
 728                      $index_to_splice = strrpos( $gap_value, '|' ) + 1;
 729                      $slug            = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
 730                      $gap_value       = "var(--wp--preset--spacing--$slug)";
 731                  }
 732  
 733                  array_push(
 734                      $layout_styles,
 735                      array(
 736                          'selector'     => "$selector > *",
 737                          'declarations' => array(
 738                              'margin-block-start' => '0',
 739                              'margin-block-end'   => '0',
 740                          ),
 741                      ),
 742                      array(
 743                          'selector'     => "$selector > * + *",
 744                          'declarations' => array(
 745                              'margin-block-start' => $gap_value,
 746                              'margin-block-end'   => '0',
 747                          ),
 748                      )
 749                  );
 750              }
 751          }
 752      } elseif ( 'flex' === $layout_type ) {
 753          $layout_orientation = $layout_for_styles['orientation'] ?? 'horizontal';
 754  
 755          $justify_content_options = array(
 756              'left'   => 'flex-start',
 757              'right'  => 'flex-end',
 758              'center' => 'center',
 759          );
 760  
 761          $vertical_alignment_options = array(
 762              'top'    => 'flex-start',
 763              'center' => 'center',
 764              'bottom' => 'flex-end',
 765          );
 766  
 767          if ( 'horizontal' === $layout_orientation ) {
 768              $justify_content_options    += array( 'space-between' => 'space-between' );
 769              $vertical_alignment_options += array( 'stretch' => 'stretch' );
 770          } else {
 771              $justify_content_options    += array( 'stretch' => 'stretch' );
 772              $vertical_alignment_options += array( 'space-between' => 'space-between' );
 773          }
 774  
 775          /*
 776           * Styles should be output either if there are no viewport overrides (this is the default case), or if the user has set a new viewport-specific
 777           * value for any of the flex properties.
 778           */
 779          $should_output_flex_wrap          = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
 780          $should_output_flex_orientation   = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
 781          $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
 782          $should_output_flex_alignment     = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
 783  
 784          if ( $should_output_flex_wrap && ! empty( $layout_for_styles['flexWrap'] ) && 'nowrap' === $layout_for_styles['flexWrap'] ) {
 785              $layout_styles[] = array(
 786                  'selector'     => $selector,
 787                  'declarations' => array( 'flex-wrap' => 'nowrap' ),
 788              );
 789          }
 790  
 791          if ( $has_block_gap_support && $should_output_block_gap && isset( $gap_value ) ) {
 792              $combined_gap_value = '';
 793              $gap_sides          = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
 794  
 795              foreach ( $gap_sides as $gap_side ) {
 796                  $process_value = $gap_value;
 797                  if ( is_array( $gap_value ) ) {
 798                      if ( is_array( $fallback_gap_value ) ) {
 799                          $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
 800                      } else {
 801                          $fallback_value = $fallback_gap_value;
 802                      }
 803                      $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
 804                  }
 805                  // Get spacing CSS variable from preset value if provided.
 806                  if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
 807                      $index_to_splice = strrpos( $process_value, '|' ) + 1;
 808                      $slug            = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
 809                      $process_value   = "var(--wp--preset--spacing--$slug)";
 810                  }
 811                  $combined_gap_value .= "$process_value ";
 812              }
 813              $gap_value = trim( $combined_gap_value );
 814  
 815              if ( '' !== $gap_value && ! $should_skip_gap_serialization ) {
 816                  $layout_styles[] = array(
 817                      'selector'     => $selector,
 818                      'declarations' => array( 'gap' => $gap_value ),
 819                  );
 820              }
 821          }
 822  
 823          $flex_justify_content    = $layout_for_styles['justifyContent'] ?? null;
 824          $flex_vertical_alignment = $layout_for_styles['verticalAlignment'] ?? null;
 825  
 826          if ( 'horizontal' === $layout_orientation ) {
 827              /*
 828               * `row` is the flex default, so the base layout never declares it. A viewport
 829               * override that switches a vertical base layout to horizontal has to declare
 830               * it explicitly, otherwise the base `flex-direction: column` keeps applying.
 831               */
 832              if ( null !== $viewport_overrides && $has_viewport_property_override( 'orientation' ) ) {
 833                  $layout_styles[] = array(
 834                      'selector'     => $selector,
 835                      'declarations' => array( 'flex-direction' => 'row' ),
 836                  );
 837              }
 838              /*
 839               * Add this style only if is not empty for backwards compatibility,
 840               * since we intend to convert blocks that had flex layout implemented
 841               * by custom css.
 842               */
 843              if ( $should_output_flex_justification && ! empty( $flex_justify_content ) && is_string( $flex_justify_content ) && array_key_exists( $flex_justify_content, $justify_content_options ) ) {
 844                  $layout_styles[] = array(
 845                      'selector'     => $selector,
 846                      'declarations' => array( 'justify-content' => $justify_content_options[ $flex_justify_content ] ),
 847                  );
 848              }
 849  
 850              if ( $should_output_flex_alignment && ! empty( $flex_vertical_alignment ) && is_string( $flex_vertical_alignment ) && array_key_exists( $flex_vertical_alignment, $vertical_alignment_options ) ) {
 851                  $layout_styles[] = array(
 852                      'selector'     => $selector,
 853                      'declarations' => array( 'align-items' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
 854                  );
 855              }
 856          } else {
 857              if ( $should_output_flex_orientation ) {
 858                  $layout_styles[] = array(
 859                      'selector'     => $selector,
 860                      'declarations' => array( 'flex-direction' => 'column' ),
 861                  );
 862              }
 863              if ( $should_output_flex_justification && ! empty( $flex_justify_content ) && is_string( $flex_justify_content ) && array_key_exists( $flex_justify_content, $justify_content_options ) ) {
 864                  $layout_styles[] = array(
 865                      'selector'     => $selector,
 866                      'declarations' => array( 'align-items' => $justify_content_options[ $flex_justify_content ] ),
 867                  );
 868              } elseif ( $should_output_flex_justification ) {
 869                  $layout_styles[] = array(
 870                      'selector'     => $selector,
 871                      'declarations' => array( 'align-items' => 'flex-start' ),
 872                  );
 873              }
 874              if ( $should_output_flex_alignment && ! empty( $flex_vertical_alignment ) && is_string( $flex_vertical_alignment ) && array_key_exists( $flex_vertical_alignment, $vertical_alignment_options ) ) {
 875                  $layout_styles[] = array(
 876                      'selector'     => $selector,
 877                      'declarations' => array( 'justify-content' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
 878                  );
 879              }
 880          }
 881      } elseif ( 'grid' === $layout_type ) {
 882          /*
 883           * Column and row counts are whole numbers, for the same reason as the grid line
 884           * numbers in wp_get_child_layout_style_rules().
 885           */
 886          $column_count_attr = $layout_for_styles['columnCount'] ?? null;
 887          $column_count      = is_numeric( $column_count_attr ) ? (int) $column_count_attr : null;
 888          $row_count_attr    = $layout_for_styles['rowCount'] ?? null;
 889          $row_count         = is_numeric( $row_count_attr ) ? (int) $row_count_attr : null;
 890  
 891          /*
 892           * If the gap value is an array, we use the "left" value because it represents the horizontal gap, which
 893           * is the relevant one for computation of responsive grid columns.
 894           */
 895          if ( is_array( $fallback_gap_value ) ) {
 896              $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
 897          } else {
 898              $responsive_gap_value = $fallback_gap_value;
 899          }
 900  
 901          if ( $has_block_gap_support && isset( $gap_value ) ) {
 902              $combined_gap_value = '';
 903              $gap_sides          = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
 904  
 905              foreach ( $gap_sides as $gap_side ) {
 906                  $process_value = $gap_value;
 907                  if ( is_array( $gap_value ) ) {
 908                      if ( is_array( $fallback_gap_value ) ) {
 909                          $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
 910                      } else {
 911                          $fallback_value = $fallback_gap_value;
 912                      }
 913                      $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
 914                  }
 915                  // Get spacing CSS variable from preset value if provided.
 916                  if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
 917                      $index_to_splice = strrpos( $process_value, '|' ) + 1;
 918                      $slug            = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
 919                      $process_value   = "var(--wp--preset--spacing--$slug)";
 920                  }
 921                  if ( ! is_array( $gap_value ) || 'left' === $gap_side ) {
 922                      $responsive_gap_value = $process_value;
 923                  }
 924                  $combined_gap_value .= "$process_value ";
 925              }
 926              $gap_value = trim( $combined_gap_value );
 927          }
 928  
 929          // Ensure 0 values have a unit so they work in calc().
 930          if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
 931              $responsive_gap_value = '0px';
 932          }
 933  
 934          /*
 935           * Styles should be output either if there are no viewport overrides (this is the default case), or if the user has set a new viewport-specific
 936           * value for any of the grid properties.
 937           */
 938          $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' ) || $has_viewport_property_override( 'autoFit' );
 939          $uses_gap_in_grid_columns   = ! empty( $column_count ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
 940          if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
 941              $should_output_grid_columns = true;
 942          }
 943  
 944          $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $column_count ) && ! empty( $row_count );
 945          $grid_declarations       = array();
 946  
 947          /*
 948           * When enabled, columns stretch to fill the available space using
 949           * `auto-fit`; otherwise empty tracks are preserved with `auto-fill`.
 950           */
 951          $auto_placement = ! empty( $layout_for_styles['autoFit'] ) ? 'auto-fit' : 'auto-fill';
 952  
 953          if ( $should_output_grid_columns && ! empty( $column_count ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
 954              $max_value                                  = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $column_count . ' - 1))) /' . $column_count . ')';
 955              $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(' . $max_value . ', 1fr))';
 956          } elseif ( $should_output_grid_columns && ! empty( $column_count ) ) {
 957              $grid_declarations['grid-template-columns'] = 'repeat(' . $column_count . ', minmax(0, 1fr))';
 958          } elseif ( $should_output_grid_columns ) {
 959              $minimum_column_width                       = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
 960              $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
 961          }
 962  
 963          if ( ! empty( $grid_declarations ) ) {
 964              $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
 965              if ( empty( $column_count ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
 966                  if ( null === $viewport_overrides || ! $base_has_container_type ) {
 967                      $grid_declarations['container-type'] = 'inline-size';
 968                  }
 969              }
 970              $layout_styles[] = array(
 971                  'selector'     => $selector,
 972                  'declarations' => $grid_declarations,
 973              );
 974          }
 975  
 976          if ( $should_output_grid_rows ) {
 977              $layout_styles[] = array(
 978                  'selector'     => $selector,
 979                  'declarations' => array( 'grid-template-rows' => 'repeat(' . $row_count . ', minmax(1rem, auto))' ),
 980              );
 981          }
 982  
 983          if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
 984              $layout_styles[] = array(
 985                  'selector'     => $selector,
 986                  'declarations' => array( 'gap' => $gap_value ),
 987              );
 988          }
 989      }
 990  
 991      if ( ! empty( $layout_styles ) ) {
 992          if ( ! empty( $rules_group ) ) {
 993              foreach ( $layout_styles as $index => $layout_style ) {
 994                  $layout_styles[ $index ]['rules_group'] = $rules_group;
 995              }
 996          }
 997  
 998          /*
 999           * Add to the style engine store to enqueue and render layout styles.
1000           * Return compiled layout styles to retain backwards compatibility.
1001           * Since https://github.com/WordPress/gutenberg/pull/42452,
1002           * wp_enqueue_block_support_styles is no longer called in this block supports file.
1003           */
1004          return wp_style_engine_get_stylesheet_from_css_rules(
1005              $layout_styles,
1006              array(
1007                  'context'  => 'block-supports',
1008                  'prettify' => false,
1009              )
1010          );
1011      }
1012  
1013      return '';
1014  }
1015  
1016  /**
1017   * Renders the layout config to the block wrapper.
1018   *
1019   * @since 5.8.0
1020   * @since 6.3.0 Adds compound class to layout wrapper for global spacing styles.
1021   * @since 6.3.0 Check for layout support via the `layout` key with fallback to `__experimentalLayout`.
1022   * @since 6.6.0 Removed duplicate container class from layout styles.
1023   * @access private
1024   *
1025   * @param string $block_content Rendered block content.
1026   * @param array  $block         Block object.
1027   * @return string Filtered block content.
1028   */
1029  function wp_render_layout_support_flag( $block_content, $block ) {
1030      $block_type            = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
1031      $block_supports_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false );
1032      $style_attr            = $block['attrs']['style'] ?? array();
1033      /*
1034       * A block with no layout support and no style attribute at all cannot
1035       * produce layout output, so return before resolving global settings.
1036       *
1037       * Resolving settings is not read-only: on a cold cache it queries the
1038       * user's `wp_global_styles` post, which fires `the_posts`. A callback on
1039       * that hook that renders blocks re-enters this filter, and the content it
1040       * renders at that point is the global styles post itself, which parses to a
1041       * single block with no name and no attributes. Without this return that
1042       * block resolves settings again and the recursion has no base case.
1043       */
1044      if ( ! $block_supports_layout && empty( $style_attr ) ) {
1045          return $block_content;
1046      }
1047  
1048      $global_settings          = wp_get_global_settings();
1049      $viewport_settings        = $global_settings['viewport'] ?? null;
1050      $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
1051      $child_layout             = $style_attr['layout'] ?? null;
1052  
1053      /*
1054       * Collect responsive viewport child layout overrides so that a block with
1055       * only responsive child layout (no base child layout) is still processed.
1056       */
1057      $viewport_child_layouts = array();
1058      foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1059          $viewport_child = wp_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
1060  
1061          if ( ! empty( $viewport_child ) ) {
1062              $viewport_child_layouts[ $breakpoint ] = array(
1063                  'media_query'  => $media_query,
1064                  'child_layout' => $viewport_child,
1065              );
1066          }
1067      }
1068  
1069      if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
1070          return $block_content;
1071      }
1072  
1073      $outer_class_names = array();
1074  
1075      // Child layout specific logic.
1076      if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
1077          $base_child_layout = wp_get_layout_child_values( $child_layout );
1078          $parent_layout     = $block['parentLayout'] ?? array();
1079          /*
1080           * Generates a unique class for child block layout styles.
1081           *
1082           * To ensure consistent class generation across different page renders,
1083           * only properties that affect layout styling are used. These properties
1084           * come from `$block['attrs']['style']['layout']`, viewport overrides in
1085           * `$block['attrs']['style'][$breakpoint]['layout']`, and `$block['parentLayout']`.
1086           *
1087           * As long as these properties coincide, the generated class will be the same.
1088           */
1089          $container_content_hash_input = array(
1090              'layout'       => $base_child_layout,
1091              'parentLayout' => array_intersect_key(
1092                  $parent_layout,
1093                  array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
1094              ),
1095          );
1096  
1097          foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
1098              $container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
1099          }
1100  
1101          $container_content_class = wp_unique_id_from_values(
1102              $container_content_hash_input,
1103              'wp-container-content-'
1104          );
1105  
1106          $child_layout_styles = wp_get_child_layout_style_rules( ".$container_content_class", $base_child_layout, $parent_layout );
1107  
1108          /*
1109           * Emit responsive child layout CSS using the same container-content class
1110           * so that base and responsive child layout share the exact same selector.
1111           */
1112          foreach ( $viewport_child_layouts as $viewport_data ) {
1113              $viewport_child_styles = wp_get_child_layout_style_rules(
1114                  ".$container_content_class",
1115                  $base_child_layout,
1116                  $parent_layout,
1117                  $viewport_data['child_layout']
1118              );
1119  
1120              foreach ( $viewport_child_styles as $index => $rule ) {
1121                  $viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
1122              }
1123  
1124              $child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
1125          }
1126  
1127          /*
1128           * Add to the style engine store to enqueue and render layout styles.
1129           * Return styles here just to check if any exist.
1130           */
1131          $child_css = wp_style_engine_get_stylesheet_from_css_rules(
1132              $child_layout_styles,
1133              array(
1134                  'context'  => 'block-supports',
1135                  'prettify' => false,
1136              )
1137          );
1138  
1139          if ( $child_css ) {
1140              $outer_class_names[] = $container_content_class;
1141          }
1142      }
1143  
1144      // Prep the processor for modifying the block output.
1145      $processor = new WP_HTML_Tag_Processor( $block_content );
1146  
1147      // Having no tags implies there are no tags onto which to add class names.
1148      if ( ! $processor->next_tag() ) {
1149          return $block_content;
1150      }
1151  
1152      /*
1153       * A block may not support layout but still be affected by a parent block's layout.
1154       *
1155       * In these cases add the appropriate class names and then return early; there's
1156       * no need to investigate on this block whether additional layout constraints apply.
1157       */
1158      if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
1159          foreach ( $outer_class_names as $class_name ) {
1160              $processor->add_class( $class_name );
1161          }
1162          return $processor->get_updated_html();
1163      } elseif ( ! $block_supports_layout ) {
1164          // Ensure layout classnames are not injected if there is no layout support.
1165          return $block_content;
1166      }
1167  
1168      $fallback_layout = $block_type->supports['layout']['default'] ?? array();
1169      if ( empty( $fallback_layout ) ) {
1170          $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
1171      }
1172      $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
1173  
1174      $class_names        = array();
1175      $layout_definitions = wp_get_layout_definitions();
1176  
1177      // Set the correct layout type for blocks using legacy content width.
1178      if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
1179          $used_layout['type'] = 'constrained';
1180      }
1181  
1182      $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
1183  
1184      if (
1185          $root_padding_aware_alignments &&
1186          isset( $used_layout['type'] ) &&
1187          'constrained' === $used_layout['type']
1188      ) {
1189          $class_names[] = 'has-global-padding';
1190      }
1191  
1192      /*
1193       * The following section was added to reintroduce a small set of layout classnames that were
1194       * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
1195       * not intended to provide an extended set of classes to match all block layout attributes
1196       * here.
1197       */
1198      $orientation = $block['attrs']['layout']['orientation'] ?? null;
1199      if ( ! empty( $orientation ) && is_string( $orientation ) ) {
1200          $class_names[] = 'is-' . sanitize_title( $orientation );
1201      }
1202  
1203      $justify_content = $block['attrs']['layout']['justifyContent'] ?? null;
1204      if ( ! empty( $justify_content ) && is_string( $justify_content ) ) {
1205          $class_names[] = 'is-content-justification-' . sanitize_title( $justify_content );
1206      }
1207  
1208      $flex_wrap = $block['attrs']['layout']['flexWrap'] ?? null;
1209      if ( ! empty( $flex_wrap ) && 'nowrap' === $flex_wrap ) {
1210          $class_names[] = 'is-nowrap';
1211      }
1212  
1213      // Get classname for layout type.
1214      if ( isset( $used_layout['type'] ) && is_string( $used_layout['type'] ) ) {
1215          $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
1216      } else {
1217          $layout_classname = $layout_definitions['default']['className'] ?? '';
1218      }
1219  
1220      if ( $layout_classname && is_string( $layout_classname ) ) {
1221          $class_names[] = sanitize_title( $layout_classname );
1222      }
1223  
1224      /*
1225       * Only generate Layout styles if the theme has not opted-out.
1226       * Attribute-based Layout classnames are output in all cases.
1227       */
1228      if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
1229  
1230          $gap_value          = wp_sanitize_block_gap_value( $style_attr['spacing']['blockGap'] ?? null );
1231          $fallback_gap_value = wp_sanitize_block_gap_value(
1232              $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null
1233          ) ?? '0.5em';
1234          $block_spacing      = $style_attr['spacing'] ?? null;
1235  
1236          /*
1237           * If a block's block.json skips serialization for spacing or spacing.blockGap,
1238           * don't apply the user-defined value to the styles.
1239           */
1240          $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
1241  
1242          $block_gap             = $global_settings['spacing']['blockGap'] ?? null;
1243          $has_block_gap_support = isset( $block_gap );
1244  
1245          // Get default blockGap value from global styles for use in layouts like grid.
1246          // Check style variation first, then block-specific styles, then fall back to root styles.
1247          $block_name    = $block['blockName'] ?? '';
1248          $global_styles = wp_get_global_styles();
1249  
1250          // Check if the block has an active style variation with a blockGap value.
1251          // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
1252          $variation_block_gap_value = null;
1253          $block_class_name          = is_string( $block['attrs']['className'] ?? null )
1254              ? $block['attrs']['className']
1255              : '';
1256          if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1257              $styles_registry   = WP_Block_Styles_Registry::get_instance();
1258              $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1259              $variation_name    = wp_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
1260              if ( $variation_name ) {
1261                  $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1262              }
1263          }
1264  
1265          $global_block_gap_candidates = array(
1266              $variation_block_gap_value,
1267              $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? null,
1268              $global_styles['spacing']['blockGap'] ?? null,
1269          );
1270          $global_block_gap_value      = null;
1271          foreach ( $global_block_gap_candidates as $candidate_gap_value ) {
1272              $candidate_gap_value = wp_sanitize_block_gap_value( $candidate_gap_value );
1273              if ( null !== $candidate_gap_value ) {
1274                  $global_block_gap_value = $candidate_gap_value;
1275                  break;
1276              }
1277          }
1278  
1279          if ( null !== $global_block_gap_value ) {
1280              $fallback_gap_value = $global_block_gap_value;
1281          }
1282  
1283          $container_class_hash_input = array(
1284              $used_layout,
1285              $has_block_gap_support,
1286              $gap_value,
1287              $should_skip_gap_serialization,
1288              $fallback_gap_value,
1289              $block_spacing,
1290          );
1291  
1292          foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1293              $viewport_style = $style_attr[ $breakpoint ] ?? null;
1294              if ( ! is_array( $viewport_style ) ) {
1295                  continue;
1296              }
1297  
1298              $viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
1299              if ( ! empty( $viewport_container_layout ) ) {
1300                  $container_class_hash_input[] = array(
1301                      'breakpoint' => $breakpoint,
1302                      'layout'     => $viewport_container_layout,
1303                  );
1304              }
1305  
1306              if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
1307                  $container_class_hash_input[] = array(
1308                      'breakpoint' => $breakpoint,
1309                      'blockGap'   => wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
1310                  );
1311              }
1312          }
1313  
1314          /*
1315           * Generates a unique ID based on all the data required to obtain the
1316           * corresponding layout style. Keeps the CSS class names the same
1317           * even for different blocks on different places, as long as they have
1318           * the same layout definition. Makes the CSS class names stable across
1319           * paginations for features like the enhanced pagination of the Query block.
1320           */
1321          $container_class = wp_unique_id_from_values(
1322              $container_class_hash_input,
1323              'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
1324          );
1325  
1326          $style = wp_get_layout_style(
1327              ".$container_class",
1328              $used_layout,
1329              $has_block_gap_support,
1330              $gap_value,
1331              $should_skip_gap_serialization,
1332              $fallback_gap_value,
1333              $block_spacing
1334          );
1335  
1336          /*
1337           * Emit responsive container layout styles using the same $container_class
1338           * selector as the base layout so they target the inner block wrapper.
1339           */
1340          foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1341              $viewport_style = $style_attr[ $breakpoint ] ?? null;
1342              if ( ! is_array( $viewport_style ) ) {
1343                  continue;
1344              }
1345  
1346              $viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
1347              $has_viewport_layout       = ! empty( $viewport_container_layout );
1348              $has_viewport_block_gap    = isset( $viewport_style['spacing']['blockGap'] );
1349  
1350              if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
1351                  continue;
1352              }
1353  
1354              $viewport_gap_value = $has_viewport_block_gap
1355                  ? wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
1356                  : $gap_value;
1357  
1358              $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
1359                  ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
1360                  : $block_spacing;
1361  
1362              $viewport_styles = wp_get_layout_style(
1363                  ".$container_class",
1364                  $used_layout,
1365                  $has_block_gap_support,
1366                  $viewport_gap_value,
1367                  $should_skip_gap_serialization,
1368                  $fallback_gap_value,
1369                  $viewport_block_spacing,
1370                  array(
1371                      'rules_group'            => $media_query,
1372                      'viewport_overrides'     => $viewport_container_layout,
1373                      'has_block_gap_override' => $has_viewport_block_gap,
1374                  )
1375              );
1376  
1377              if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
1378                  $class_names[] = $container_class;
1379              }
1380          }
1381  
1382          // Only add container class and enqueue block support styles if unique styles were generated.
1383          if ( ! empty( $style ) ) {
1384              $class_names[] = $container_class;
1385          }
1386      }
1387  
1388      // Add combined layout and block classname for global styles to hook onto.
1389      $split_block_name = explode( '/', $block['blockName'] );
1390      $full_block_name  = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
1391      $class_names[]    = 'wp-block-' . $full_block_name . '-' . $layout_classname;
1392  
1393      // Add classes to the outermost HTML tag if necessary.
1394      if ( ! empty( $outer_class_names ) ) {
1395          foreach ( $outer_class_names as $outer_class_name ) {
1396              $processor->add_class( $outer_class_name );
1397          }
1398      }
1399  
1400      /**
1401       * Attempts to refer to the inner-block wrapping element by its class attribute.
1402       *
1403       * When examining a block's inner content, if a block has inner blocks, then
1404       * the first content item will likely be a text (HTML) chunk immediately
1405       * preceding the inner blocks. The last HTML tag in that chunk would then be
1406       * an opening tag for an element that wraps the inner blocks.
1407       *
1408       * There's no reliable way to associate this wrapper in $block_content because
1409       * it may have changed during the rendering pipeline (as inner contents is
1410       * provided before rendering) and through previous filters. In many cases,
1411       * however, the `class` attribute will be a good-enough identifier, so this
1412       * code finds the last tag in that chunk and stores the `class` attribute
1413       * so that it can be used later when working through the rendered block output
1414       * to identify the wrapping element and add the remaining class names to it.
1415       *
1416       * It's also possible that no inner block wrapper even exists. If that's the
1417       * case this code could apply the class names to an invalid element.
1418       *
1419       * Example:
1420       *
1421       *     $block['innerBlocks']  = array( $list_item );
1422       *     $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
1423       *
1424       *     // After rendering, the initial contents may have been modified by other renderers or filters.
1425       *     $block_content = <<<HTML
1426       *         <figure>
1427       *             <ul class="annotated-list list-wrapper is-unordered">
1428       *                 <li>Code</li>
1429       *             </ul><figcaption>It's a list!</figcaption>
1430       *         </figure>
1431       *     HTML;
1432       *
1433       * Although it is possible that the original block-wrapper classes are changed in $block_content
1434       * from how they appear in $block['innerContent'], it's likely that the original class attributes
1435       * are still present in the wrapper as they are in this example. Frequently, additional classes
1436       * will also be present; rarely should classes be removed.
1437       *
1438       * @todo Find a better way to match the first inner block. If it's possible to identify where the
1439       *       first inner block starts, then it will be possible to find the last tag before it starts
1440       *       and then that tag, if an opening tag, can be solidly identified as a wrapping element.
1441       *       Can some unique value or class or ID be added to the inner blocks when they process
1442       *       so that they can be extracted here safely without guessing? Can the block rendering function
1443       *       return information about where the rendered inner blocks start?
1444       *
1445       * @var string|null
1446       */
1447      $inner_block_wrapper_classes = null;
1448      $first_chunk                 = $block['innerContent'][0] ?? null;
1449      if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
1450          $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
1451          /*
1452           * Use a stack to track open elements as tags are visited. Void elements
1453           * (those without a matching closing tag) are excluded so they don't
1454           * accumulate on the stack. At the end of the chunk, every element still
1455           * on the stack is unclosed — meaning its closing tag lives in a later
1456           * innerContent entry alongside the inner blocks, which makes it the
1457           * inner-block container. Elements that open and close within this chunk
1458           * are siblings that precede the inner blocks and should be ignored.
1459           * The last unclosed element with a class attribute is the best candidate
1460           * for the inner-block wrapper.
1461           */
1462          $tag_stack = array();
1463          while ( $first_chunk_processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
1464              if ( $first_chunk_processor->is_tag_closer() ) {
1465                  array_pop( $tag_stack );
1466              } elseif ( ! WP_HTML_Processor::is_void( $first_chunk_processor->get_tag() ) ) {
1467                  $tag_stack[] = $first_chunk_processor->get_attribute( 'class' );
1468              }
1469          }
1470          foreach ( array_reverse( $tag_stack ) as $class_attribute ) {
1471              if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
1472                  $inner_block_wrapper_classes = $class_attribute;
1473                  break;
1474              }
1475          }
1476      }
1477  
1478      /*
1479       * If necessary, advance to what is likely to be an inner block wrapper tag.
1480       *
1481       * This advances until it finds the first tag containing the original class
1482       * attribute from above. If none is found it will scan to the end of the block
1483       * and fail to add any class names.
1484       *
1485       * If there is no block wrapper it won't advance at all, in which case the
1486       * class names will be added to the first and outermost tag of the block.
1487       * For cases where this outermost tag is the only tag surrounding inner
1488       * blocks then the outer wrapper and inner wrapper are the same.
1489       */
1490      do {
1491          if ( ! $inner_block_wrapper_classes ) {
1492              break;
1493          }
1494  
1495          $class_attribute = $processor->get_attribute( 'class' );
1496          if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
1497              break;
1498          }
1499      } while ( $processor->next_tag() );
1500  
1501      // Add the remaining class names.
1502      foreach ( $class_names as $class_name ) {
1503          $processor->add_class( $class_name );
1504      }
1505  
1506      return $processor->get_updated_html();
1507  }
1508  
1509  /**
1510   * Check if the parent block exists and if it has a layout attribute.
1511   * If it does, add the parent layout to the parsed block
1512   *
1513   * @since 6.6.0
1514   * @access private
1515   *
1516   * @param array    $parsed_block The parsed block.
1517   * @param array    $source_block The source block.
1518   * @param WP_Block $parent_block The parent block.
1519   * @return array The parsed block with parent layout attribute if it exists.
1520   */
1521  function wp_add_parent_layout_to_parsed_block( $parsed_block, $source_block, $parent_block ) {
1522      if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) {
1523          $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout'];
1524      }
1525      return $parsed_block;
1526  }
1527  
1528  add_filter( 'render_block_data', 'wp_add_parent_layout_to_parsed_block', 10, 3 );
1529  
1530  // Register the block support.
1531  WP_Block_Supports::get_instance()->register(
1532      'layout',
1533      array(
1534          'register_attribute' => 'wp_register_layout_support',
1535      )
1536  );
1537  add_filter( 'render_block', 'wp_render_layout_support_flag', 10, 2 );
1538  
1539  /**
1540   * For themes without theme.json file, make sure
1541   * to restore the inner div for the group block
1542   * to avoid breaking styles relying on that div.
1543   *
1544   * @since 5.8.0
1545   * @since 6.6.1 Removed inner container from Grid variations.
1546   * @access private
1547   *
1548   * @param string $block_content Rendered block content.
1549   * @param array  $block         Block object.
1550   * @return string Filtered block content.
1551   */
1552  function wp_restore_group_inner_container( $block_content, $block ) {
1553      $tag_name_attr                    = $block['attrs']['tagName'] ?? null;
1554      $tag_name                         = is_string( $tag_name_attr ) ? $tag_name_attr : 'div';
1555      $group_with_inner_container_regex = sprintf(
1556          '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1557          preg_quote( $tag_name, '/' )
1558      );
1559  
1560      if (
1561          wp_theme_has_theme_json() ||
1562          1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
1563          ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
1564      ) {
1565          return $block_content;
1566      }
1567  
1568      /*
1569       * This filter runs after the layout classnames have been added to the block, so they
1570       * have to be removed from the outer wrapper and then added to the inner.
1571       */
1572      $layout_classes = array();
1573      $processor      = new WP_HTML_Tag_Processor( $block_content );
1574  
1575      if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
1576          foreach ( $processor->class_list() as $class_name ) {
1577              if ( str_contains( $class_name, 'is-layout-' ) ) {
1578                  $layout_classes[] = $class_name;
1579                  $processor->remove_class( $class_name );
1580              }
1581          }
1582      }
1583  
1584      $content_without_layout_classes = $processor->get_updated_html();
1585      $replace_regex                  = sprintf(
1586          '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
1587          preg_quote( $tag_name, '/' )
1588      );
1589      $updated_content                = preg_replace_callback(
1590          $replace_regex,
1591          static function ( $matches ) {
1592              return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
1593          },
1594          $content_without_layout_classes
1595      );
1596  
1597      // Add layout classes to inner wrapper.
1598      if ( ! empty( $layout_classes ) ) {
1599          $processor = new WP_HTML_Tag_Processor( $updated_content );
1600          if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) {
1601              foreach ( $layout_classes as $class_name ) {
1602                  $processor->add_class( $class_name );
1603              }
1604          }
1605          $updated_content = $processor->get_updated_html();
1606      }
1607      return $updated_content;
1608  }
1609  
1610  add_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10, 2 );
1611  
1612  /**
1613   * For themes without theme.json file, make sure
1614   * to restore the outer div for the aligned image block
1615   * to avoid breaking styles relying on that div.
1616   *
1617   * @since 6.0.0
1618   * @access private
1619   *
1620   * @param string $block_content Rendered block content.
1621   * @param array  $block         Block object.
1622   * @return string Filtered block content.
1623   */
1624  function wp_restore_image_outer_container( $block_content, $block ) {
1625      if ( wp_theme_has_theme_json() ) {
1626          return $block_content;
1627      }
1628  
1629      $figure_processor = new WP_HTML_Tag_Processor( $block_content );
1630      if (
1631          ! $figure_processor->next_tag( 'FIGURE' ) ||
1632          ! $figure_processor->has_class( 'wp-block-image' ) ||
1633          ! (
1634              $figure_processor->has_class( 'alignleft' ) ||
1635              $figure_processor->has_class( 'aligncenter' ) ||
1636              $figure_processor->has_class( 'alignright' )
1637          )
1638      ) {
1639          return $block_content;
1640      }
1641  
1642      /*
1643       * The next section of code wraps the existing figure in a new DIV element.
1644       * While doing it, it needs to transfer the layout and the additional CSS
1645       * class names from the original figure upward to the wrapper.
1646       *
1647       * Example:
1648       *
1649       *     // From this…
1650       *     <!-- wp:image {"className":"hires"} -->
1651       *     <figure class="wp-block-image wide hires">…
1652       *
1653       *     // To this…
1654       *     <div class="wp-block-image hires"><figure class="wide">…
1655       */
1656      $wrapper_processor = new WP_HTML_Tag_Processor( '<div>' );
1657      $wrapper_processor->next_token();
1658      $wrapper_processor->set_attribute(
1659          'class',
1660          is_string( $block['attrs']['className'] ?? null )
1661              ? "wp-block-image {$block['attrs']['className']}"
1662              : 'wp-block-image'
1663      );
1664  
1665      // And remove them from the existing content; it has been transferred upward.
1666      $figure_processor->remove_class( 'wp-block-image' );
1667      foreach ( $wrapper_processor->class_list() as $class_name ) {
1668          $figure_processor->remove_class( $class_name );
1669      }
1670  
1671      return "{$wrapper_processor->get_updated_html()}{$figure_processor->get_updated_html()}</div>";
1672  }
1673  
1674  add_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10, 2 );


Generated : Wed Nov 11 08:20:02 2026 Cross-referenced by PHPXref