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


Generated : Wed Jul 15 08:20:16 2026 Cross-referenced by PHPXref