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