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


Generated : Sun Sep 13 08:20:28 2026 Cross-referenced by PHPXref