[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Oct 23 08:20:34 2026 Cross-referenced by PHPXref