PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/block-supports/layout.php +262 -93 23.3.2 → trunk View file →
@@ -283,19 +283,39 @@
283 283 *
284 284 * Regex for CSS value borrowed from `safecss_filter_attr`, used here to only match
285 285 * against the value, not the CSS attribute.
286 286 *
287 - * @param string|array|null $gap_value Block gap value.
288 - * @return string|array|null Sanitized block gap value.
287 + * Numeric zero is converted to a string because it is valid CSS without a unit.
288 + * Other non-string values are rejected.
289 + *
290 + * @param mixed $gap_value Block gap value.
291 + * @return string|string[]|null Sanitized block gap value.
289 292 */
290 293 function gutenberg_sanitize_block_gap_value( $gap_value ) {
291 294 if ( is_array( $gap_value ) ) {
292 295 foreach ( $gap_value as $key => $value ) {
293 - $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
296 + $sanitized_value = gutenberg_sanitize_block_gap_value( $value );
297 + if ( ! is_string( $sanitized_value ) ) {
298 + unset( $gap_value[ $key ] );
299 + continue;
300 + }
301 + $gap_value[ $key ] = $sanitized_value;
294 302 }
295 - return $gap_value;
303 + return empty( $gap_value ) ? null : $gap_value;
296 304 }
297 305
306 + if ( ( is_int( $gap_value ) || is_float( $gap_value ) ) && 0.0 === (float) $gap_value ) {
307 + return '0';
308 + }
309 +
310 + if ( ! is_string( $gap_value ) ) {
311 + return null;
312 + }
313 +
314 + if ( '' === trim( $gap_value ) ) {
315 + return null;
316 + }
317 +
298 318 return $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
299 319 }
300 320
301 321 /**
@@ -300,11 +320,11 @@
300 320
301 321 /**
302 322 * Returns child layout styles for a block affected by its parent's layout.
303 323 *
304 - * @param string $selector CSS selector.
305 - * @param array $child_layout Child layout values.
306 - * @param array $parent_layout Parent layout values.
324 + * @param string $selector CSS selector.
325 + * @param array $child_layout Child layout values.
326 + * @param array $parent_layout Parent layout values.
307 327 * @param array|null $viewport_overrides Optional. Child viewport layout overrides to emit.
308 328 * @return array Child layout style rules.
309 329 */
310 330 function gutenberg_get_child_layout_style_rules( $selector, $child_layout, $parent_layout = array(), $viewport_overrides = null ) {
@@ -316,21 +336,62 @@
316 336 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
317 337 return array_key_exists( $property, $viewport_overrides );
318 338 };
319 339
320 - $self_stretch = $child_layout['selfStretch'] ?? null;
340 + $self_stretch = $child_layout['selfStretch'] ?? null;
341 + $base_self_stretch = $base_child_layout['selfStretch'] ?? null;
321 342
343 + /*
344 + * These are the serialized `selfStretch` values. `max` used to be called
345 + * "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`.
346 + */
347 + $flex_child_layout_values = array(
348 + 'fit' => 'fit',
349 + 'grow' => 'fill',
350 + 'max' => 'fixed',
351 + 'fixed' => 'fixedNoShrink',
352 + );
353 + $flex_size_values = array(
354 + $flex_child_layout_values['max'],
355 + $flex_child_layout_values['fixed'],
356 + );
357 +
322 358 if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( 'flexSize' ) ) {
323 - if ( 'fixed' === $self_stretch && isset( $child_layout['flexSize'] ) ) {
359 + if (
360 + null !== $viewport_overrides &&
361 + ( $flex_child_layout_values['fit'] === $self_stretch || $flex_child_layout_values['grow'] === $self_stretch ) &&
362 + in_array( $base_self_stretch, $flex_size_values, true ) &&
363 + isset( $base_child_layout['flexSize'] )
364 + ) {
365 + $child_layout_declarations['flex-basis'] = 'unset';
366 + if ( $flex_child_layout_values['fixed'] === $base_self_stretch ) {
367 + $child_layout_declarations['flex-shrink'] = 'unset';
368 + }
369 + }
370 + if ( in_array( $self_stretch, $flex_size_values, true ) && isset( $child_layout['flexSize'] ) ) {
324 371 $child_layout_declarations['flex-basis'] = $child_layout['flexSize'];
372 + if ( $flex_child_layout_values['fixed'] === $self_stretch ) {
373 + $child_layout_declarations['flex-shrink'] = '0';
374 + } elseif ( null !== $viewport_overrides && $flex_child_layout_values['fixed'] === $base_self_stretch ) {
375 + $child_layout_declarations['flex-shrink'] = 'unset';
376 + }
325 377 $child_layout_declarations['box-sizing'] = 'border-box';
326 - } elseif ( 'fill' === $self_stretch ) {
378 + } elseif ( $flex_child_layout_values['grow'] === $self_stretch ) {
327 379 $child_layout_declarations['flex-grow'] = '1';
328 380 }
329 381 }
330 382
331 - $column_start = $child_layout['columnStart'] ?? null;
332 - $column_span = $child_layout['columnSpan'] ?? null;
383 + /*
384 + * Grid line numbers and spans are whole numbers. The editor stores them as numbers, but
385 + * content saved by WordPress 6.3 to 6.6 stored them as numeric strings, and that
386 + * migration only runs when a block is parsed in JavaScript, so the front end still sees
387 + * strings. Accept any numeric value and cast it, and treat anything else as absent
388 + * because it can't render as valid CSS.
389 + */
390 + $column_start_attr = $child_layout['columnStart'] ?? null;
391 + $column_start = is_numeric( $column_start_attr ) ? (int) $column_start_attr : null;
392 + $column_span_attr = $child_layout['columnSpan'] ?? null;
393 + $column_span = is_numeric( $column_span_attr ) ? (int) $column_span_attr : null;
333 394 if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
334 395 if ( $column_start && $column_span ) {
335 396 $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
336 397 } elseif ( $column_start ) {
@@ -339,10 +400,12 @@
339 400 $child_layout_declarations['grid-column'] = "span $column_span";
340 401 }
341 402 }
342 403
343 - $row_start = $child_layout['rowStart'] ?? null;
344 - $row_span = $child_layout['rowSpan'] ?? null;
404 + $row_start_attr = $child_layout['rowStart'] ?? null;
405 + $row_start = is_numeric( $row_start_attr ) ? (int) $row_start_attr : null;
406 + $row_span_attr = $child_layout['rowSpan'] ?? null;
407 + $row_span = is_numeric( $row_span_attr ) ? (int) $row_span_attr : null;
345 408 if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
346 409 if ( $row_start && $row_span ) {
347 410 $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
348 411 } elseif ( $row_start ) {
@@ -358,10 +421,11 @@
358 421 'declarations' => $child_layout_declarations,
359 422 );
360 423 }
361 424
362 - $minimum_column_width = $parent_layout['minimumColumnWidth'] ?? null;
363 - $column_count = $parent_layout['columnCount'] ?? null;
425 + $minimum_column_width_attr = $parent_layout['minimumColumnWidth'] ?? null;
426 + $minimum_column_width = is_string( $minimum_column_width_attr ) ? $minimum_column_width_attr : null;
427 + $column_count = $parent_layout['columnCount'] ?? null;
364 428
365 429 /*
366 430 * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
367 431 * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
@@ -427,27 +491,37 @@
427 491
428 492 /**
429 493 * Generates the CSS corresponding to the provided layout.
430 494 *
431 - * @param string $selector CSS selector.
432 - * @param array $layout Layout object. The one that is passed has already checked
433 - * the existence of default block layout.
434 - * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
435 - * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null.
436 - * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
437 - * @param string|array $fallback_gap_value Optional. The block gap value to apply. If it's an array expected properties are "top" and/or "left". Default '0.5em'.
438 - * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
439 - * @param array $options Optional. Extra options for internal callers. Default empty array.
495 + * @param string $selector CSS selector.
496 + * @param array $layout Layout object. The one that is passed has already checked
497 + * the existence of default block layout.
498 + * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
499 + * @param string|string[]|int|float|null $gap_value Optional. The block gap value to apply. Only zero is accepted as a
500 + * numeric value. Default null.
501 + * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the
502 + * editor. Default false.
503 + * @param string|string[]|int|float|null $fallback_gap_value Optional. The fallback block gap value to apply. Only zero is accepted
504 + * as a numeric value. Default '0.5em'.
505 + * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
506 + * @param array $options Optional. Extra options for internal callers. Default empty array.
440 507 * @return string CSS styles, or empty string.
441 508 */
442 509 function gutenberg_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() ) {
443 - $base_layout = is_array( $layout ) ? $layout : array();
444 - $viewport_overrides = $options['viewport_overrides'] ?? null;
445 - $layout_for_styles = null === $viewport_overrides ? $base_layout : array_replace( $base_layout, $viewport_overrides );
446 - $layout_type = $base_layout['type'] ?? 'default';
447 - $rules_group = $options['rules_group'] ?? null;
448 - $has_block_gap_override = ! empty( $options['has_block_gap_override'] );
449 - $should_output_block_gap = null === $viewport_overrides || $has_block_gap_override;
510 + // Normalize here as well as at external data boundaries because this function has direct callers.
511 + $gap_value = gutenberg_sanitize_block_gap_value( $gap_value );
512 + $fallback_gap_value = gutenberg_sanitize_block_gap_value( $fallback_gap_value ) ?? '0.5em';
513 +
514 + $base_layout = is_array( $layout ) ? $layout : array();
515 + $viewport_overrides = $options['viewport_overrides'] ?? null;
516 + $layout_for_styles = null === $viewport_overrides ? $base_layout : array_replace( $base_layout, $viewport_overrides );
517 + $layout_type = $base_layout['type'] ?? 'default';
518 + $rules_group = $options['rules_group'] ?? null;
519 + $has_block_gap_override = ! empty( $options['has_block_gap_override'] );
520 + $should_output_block_gap = null === $viewport_overrides || $has_block_gap_override;
521 + // Viewport styles only store changed fields. If a field is present with null,
522 + // the user cleared a value inherited from the default viewport, so check
523 + // whether the key exists rather than whether the value is truthy.
450 524 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
451 525 return array_key_exists( $property, $viewport_overrides );
452 526 };
453 527 $layout_styles = array();
@@ -484,15 +558,40 @@
484 558 );
485 559 }
486 560 }
487 561 } elseif ( 'constrained' === $layout_type ) {
488 - $content_size = $layout_for_styles['contentSize'] ?? '';
489 - $wide_size = $layout_for_styles['wideSize'] ?? '';
490 - $justify_content = $layout_for_styles['justifyContent'] ?? 'center';
562 + // The schemas and editor UI only produce strings here, so treat a non-string
563 + // value as absent rather than casting it — it couldn't render as valid CSS anyway.
564 + $content_size_attr = $layout_for_styles['contentSize'] ?? null;
565 + $content_size = is_string( $content_size_attr ) ? $content_size_attr : '';
566 + $wide_size_attr = $layout_for_styles['wideSize'] ?? null;
567 + $wide_size = is_string( $wide_size_attr ) ? $wide_size_attr : '';
568 + $justify_content_attr = $layout_for_styles['justifyContent'] ?? null;
569 + $justify_content = is_string( $justify_content_attr ) ? $justify_content_attr : 'center';
491 570
492 - $all_max_width_value = $content_size ? $content_size : $wide_size;
493 - $wide_max_width_value = $wide_size ? $wide_size : $content_size;
571 + // Check if viewport-specific ("override") values exist. Null values are valid and mean the user cleared a value inherited from the default viewport.
572 + $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
573 + $has_content_size_override = null !== $viewport_overrides && $has_viewport_property_override( 'contentSize' );
574 + $has_wide_size_override = null !== $viewport_overrides && $has_viewport_property_override( 'wideSize' );
494 575
576 + /* 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
577 + * value for contentSize or wideSize. If a viewport clears a custom constrained size, reset to the global layout variable.
578 + */
579 + $should_output_constrained_sizes = null === $viewport_overrides || $has_content_size_override || $has_wide_size_override;
580 + $is_resetting_constrained_sizes = null !== $viewport_overrides &&
581 + (
582 + ( $has_content_size_override && ! $content_size ) ||
583 + ( $has_wide_size_override && ! $wide_size )
584 + );
585 +
586 + // If a viewport clears a custom constrained size, reset to the global layout variable.
587 + $all_max_width_value = $content_size
588 + ? $content_size
589 + : ( $wide_size && ! $has_content_size_override ? $wide_size : 'var(--wp--style--global--content-size, none)' );
590 + $wide_max_width_value = $wide_size
591 + ? $wide_size
592 + : ( $content_size && ! $has_wide_size_override ? $content_size : 'var(--wp--style--global--wide-size, none)' );
593 +
495 594 // Make sure there is a single CSS rule, and all tags are stripped for security.
496 595 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
497 596 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
498 597
@@ -498,11 +597,9 @@
498 597
499 598 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
500 599 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
501 600
502 - $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
503 - $should_output_constrained_sizes = null === $viewport_overrides || $has_viewport_property_override( 'contentSize' ) || $has_viewport_property_override( 'wideSize' );
504 - if ( $should_output_constrained_sizes && ( $content_size || $wide_size ) ) {
601 + if ( $should_output_constrained_sizes && ( $content_size || $wide_size || $is_resetting_constrained_sizes ) ) {
505 602 $content_size_declarations = array(
506 603 'max-width' => $all_max_width_value,
507 604 );
508 605
@@ -640,8 +737,11 @@
640 737 $justify_content_options += array( 'stretch' => 'stretch' );
641 738 $vertical_alignment_options += array( 'space-between' => 'space-between' );
642 739 }
643 740
741 + /* 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
742 + * value for any of the flex properties.
743 + */
644 744 $should_output_flex_wrap = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
645 745 $should_output_flex_orientation = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
646 746 $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
647 747 $should_output_flex_alignment = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
@@ -676,9 +776,9 @@
676 776 $combined_gap_value .= "$process_value ";
677 777 }
678 778 $gap_value = trim( $combined_gap_value );
679 779
680 - if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
780 + if ( '' !== $gap_value && ! $should_skip_gap_serialization ) {
681 781 $layout_styles[] = array(
682 782 'selector' => $selector,
683 783 'declarations' => array( 'gap' => $gap_value ),
684 784 );
@@ -684,25 +784,39 @@
684 784 );
685 785 }
686 786 }
687 787
788 + $flex_justify_content = $layout_for_styles['justifyContent'] ?? null;
789 + $flex_vertical_alignment = $layout_for_styles['verticalAlignment'] ?? null;
790 +
688 791 if ( 'horizontal' === $layout_orientation ) {
689 792 /*
793 + * `row` is the flex default, so the base layout never declares it. A viewport
794 + * override that switches a vertical base layout to horizontal has to declare
795 + * it explicitly, otherwise the base `flex-direction: column` keeps applying.
796 + */
797 + if ( null !== $viewport_overrides && $has_viewport_property_override( 'orientation' ) ) {
798 + $layout_styles[] = array(
799 + 'selector' => $selector,
800 + 'declarations' => array( 'flex-direction' => 'row' ),
801 + );
802 + }
803 + /*
690 804 * Add this style only if is not empty for backwards compatibility,
691 805 * since we intend to convert blocks that had flex layout implemented
692 806 * by custom css.
693 807 */
694 - if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
808 + if ( $should_output_flex_justification && ! empty( $flex_justify_content ) && is_string( $flex_justify_content ) && array_key_exists( $flex_justify_content, $justify_content_options ) ) {
695 809 $layout_styles[] = array(
696 810 'selector' => $selector,
697 - 'declarations' => array( 'justify-content' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
811 + 'declarations' => array( 'justify-content' => $justify_content_options[ $flex_justify_content ] ),
698 812 );
699 813 }
700 814
701 - if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
815 + if ( $should_output_flex_alignment && ! empty( $flex_vertical_alignment ) && is_string( $flex_vertical_alignment ) && array_key_exists( $flex_vertical_alignment, $vertical_alignment_options ) ) {
702 816 $layout_styles[] = array(
703 817 'selector' => $selector,
704 - 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
818 + 'declarations' => array( 'align-items' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
705 819 );
706 820 }
707 821 } else {
708 822 if ( $should_output_flex_orientation ) {
@@ -710,12 +824,12 @@
710 824 'selector' => $selector,
711 825 'declarations' => array( 'flex-direction' => 'column' ),
712 826 );
713 827 }
714 - if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
828 + if ( $should_output_flex_justification && ! empty( $flex_justify_content ) && is_string( $flex_justify_content ) && array_key_exists( $flex_justify_content, $justify_content_options ) ) {
715 829 $layout_styles[] = array(
716 830 'selector' => $selector,
717 - 'declarations' => array( 'align-items' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
831 + 'declarations' => array( 'align-items' => $justify_content_options[ $flex_justify_content ] ),
718 832 );
719 833 } elseif ( $should_output_flex_justification ) {
720 834 $layout_styles[] = array(
721 835 'selector' => $selector,
@@ -721,18 +835,27 @@
721 835 'selector' => $selector,
722 836 'declarations' => array( 'align-items' => 'flex-start' ),
723 837 );
724 838 }
725 - if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
839 + if ( $should_output_flex_alignment && ! empty( $flex_vertical_alignment ) && is_string( $flex_vertical_alignment ) && array_key_exists( $flex_vertical_alignment, $vertical_alignment_options ) ) {
726 840 $layout_styles[] = array(
727 841 'selector' => $selector,
728 - 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
842 + 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
729 843 );
730 844 }
731 845 }
732 846 } elseif ( 'grid' === $layout_type ) {
733 847 /*
734 - * If the gap value is an array, we use the "left" value because it represents the vertical gap, which
848 + * Column and row counts are whole numbers, for the same reason as the grid line
849 + * numbers in gutenberg_get_child_layout_style_rules().
850 + */
851 + $column_count_attr = $layout_for_styles['columnCount'] ?? null;
852 + $column_count = is_numeric( $column_count_attr ) ? (int) $column_count_attr : null;
853 + $row_count_attr = $layout_for_styles['rowCount'] ?? null;
854 + $row_count = is_numeric( $row_count_attr ) ? (int) $row_count_attr : null;
855 +
856 + /*
857 + * If the gap value is an array, we use the "left" value because it represents the horizontal gap, which
735 858 * is the relevant one for computation of responsive grid columns.
736 859 */
737 860 if ( is_array( $fallback_gap_value ) ) {
738 861 $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
@@ -759,12 +882,14 @@
759 882 $index_to_splice = strrpos( $process_value, '|' ) + 1;
760 883 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
761 884 $process_value = "var(--wp--preset--spacing--$slug)";
762 885 }
886 + if ( ! is_array( $gap_value ) || 'left' === $gap_side ) {
887 + $responsive_gap_value = $process_value;
888 + }
763 889 $combined_gap_value .= "$process_value ";
764 890 }
765 - $gap_value = trim( $combined_gap_value );
766 - $responsive_gap_value = $gap_value;
891 + $gap_value = trim( $combined_gap_value );
767 892 }
768 893
769 894 // Ensure 0 values have a unit so they work in calc().
770 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
@@ -770,30 +895,38 @@
770 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
771 896 $responsive_gap_value = '0px';
772 897 }
773 898
774 - $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' );
775 - $uses_gap_in_grid_columns = ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
899 + /* 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
900 + * value for any of the grid properties.
901 + */
902 + $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' ) || $has_viewport_property_override( 'autoFit' );
903 + $uses_gap_in_grid_columns = ! empty( $column_count ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
776 904 if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
777 905 $should_output_grid_columns = true;
778 906 }
779 907
780 - $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['rowCount'] );
908 + $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $column_count ) && ! empty( $row_count );
781 909 $grid_declarations = array();
782 910
783 - if ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
784 - $max_value = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout_for_styles['columnCount'] . ' - 1))) /' . $layout_for_styles['columnCount'] . ')';
785 - $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))';
786 - } elseif ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) ) {
787 - $grid_declarations['grid-template-columns'] = 'repeat(' . $layout_for_styles['columnCount'] . ', minmax(0, 1fr))';
911 + /* When enabled, columns stretch to fill the available space using
912 + * `auto-fit`; otherwise empty tracks are preserved with `auto-fill`.
913 + */
914 + $auto_placement = ! empty( $layout_for_styles['autoFit'] ) ? 'auto-fit' : 'auto-fill';
915 +
916 + if ( $should_output_grid_columns && ! empty( $column_count ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
917 + $max_value = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $column_count . ' - 1))) /' . $column_count . ')';
918 + $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(' . $max_value . ', 1fr))';
919 + } elseif ( $should_output_grid_columns && ! empty( $column_count ) ) {
920 + $grid_declarations['grid-template-columns'] = 'repeat(' . $column_count . ', minmax(0, 1fr))';
788 921 } elseif ( $should_output_grid_columns ) {
789 922 $minimum_column_width = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
790 - $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
923 + $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
791 924 }
792 925
793 926 if ( ! empty( $grid_declarations ) ) {
794 927 $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
795 - if ( empty( $layout_for_styles['columnCount'] ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
928 + if ( empty( $column_count ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
796 929 if ( null === $viewport_overrides || ! $base_has_container_type ) {
797 930 $grid_declarations['container-type'] = 'inline-size';
798 931 }
799 932 }
@@ -805,9 +938,9 @@
805 938
806 939 if ( $should_output_grid_rows ) {
807 940 $layout_styles[] = array(
808 941 'selector' => $selector,
809 - 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout_for_styles['rowCount'] . ', minmax(1rem, auto))' ),
942 + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $row_count . ', minmax(1rem, auto))' ),
810 943 );
811 944 }
812 945
813 946 if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
@@ -851,10 +984,10 @@
851 984 * and increments on each call. The returned value is not universally unique,
852 985 * but it is unique across the life of the PHP process and it's stable per
853 986 * prefix.
854 987 *
855 - * @param string $prefix Prefix for the returned ID.
856 - * @return string Incremental ID per prefix.
988 + * @param string $prefix Prefix for the returned ID.
989 + * @return string Incremental ID per prefix.
857 990 */
858 991 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
859 992 static $id_counters = array();
860 993 if ( ! array_key_exists( $prefix, $id_counters ) ) {
@@ -883,18 +1016,37 @@
883 1016
884 1017 /**
885 1018 * Renders the layout config to the block wrapper.
886 1019 *
887 - * @param string $block_content Rendered block content.
888 - * @param array $block Block object.
889 - * @return string Filtered block content.
1020 + * @param string $block_content Rendered block content.
1021 + * @param array $block Block object.
1022 + * @return string Filtered block content.
890 1023 */
891 1024 function gutenberg_render_layout_support_flag( $block_content, $block ) {
892 - static $global_styles = null;
893 -
894 1025 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
895 1026 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
896 - $style_attr = $block['attrs']['style'] ?? array();
1027 + $style_attr = gutenberg_resolve_style_state_aliases(
1028 + $block['attrs']['style'] ?? array(),
1029 + $block['blockName']
1030 + );
1031 + /*
1032 + * A block with no layout support and no style attribute at all cannot
1033 + * produce layout output, so return before resolving global settings.
1034 + *
1035 + * Resolving settings is not read-only: on a cold cache it queries the
1036 + * user's `wp_global_styles` post, which fires `the_posts`. A callback on
1037 + * that hook that renders blocks re-enters this filter, and the content it
1038 + * renders at that point is the global styles post itself, which parses to a
1039 + * single block with no name and no attributes. Without this return that
1040 + * block resolves settings again and the recursion has no base case.
1041 + */
1042 + if ( ! $block_supports_layout && empty( $style_attr ) ) {
1043 + return $block_content;
1044 + }
1045 +
1046 + $global_settings = gutenberg_get_global_settings();
1047 + $viewport_settings = $global_settings['viewport'] ?? null;
1048 + $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );
897 1049 // If there is any value in style -> layout, the block has a child layout.
898 1050 $child_layout = $style_attr['layout'] ?? null;
899 1051
900 1052 // Collect responsive viewport child layout overrides so that a block with
@@ -899,9 +1051,9 @@
899 1051
900 1052 // Collect responsive viewport child layout overrides so that a block with
901 1053 // only responsive child layout (no base child layout) is still processed.
902 1054 $viewport_child_layouts = array();
903 - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1055 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
904 1056 $viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
905 1057 if ( ! empty( $viewport_child ) ) {
906 1058 $viewport_child_layouts[ $breakpoint ] = array(
907 1059 'media_query' => $media_query,
@@ -1009,9 +1161,8 @@
1009 1161 // Ensure layout classnames are not injected if there is no layout support.
1010 1162 return $block_content;
1011 1163 }
1012 1164
1013 - $global_settings = gutenberg_get_global_settings();
1014 1165 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
1015 1166 if ( empty( $fallback_layout ) ) {
1016 1167 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
1017 1168 }
@@ -1036,22 +1187,25 @@
1036 1187 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
1037 1188 * not intended to provide an extended set of classes to match all block layout attributes
1038 1189 * here.
1039 1190 */
1040 - if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
1041 - $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
1191 + $orientation = $block['attrs']['layout']['orientation'] ?? null;
1192 + if ( ! empty( $orientation ) && is_string( $orientation ) ) {
1193 + $class_names[] = 'is-' . sanitize_title( $orientation );
1042 1194 }
1043 1195
1044 - if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
1045 - $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
1196 + $justify_content = $block['attrs']['layout']['justifyContent'] ?? null;
1197 + if ( ! empty( $justify_content ) && is_string( $justify_content ) ) {
1198 + $class_names[] = 'is-content-justification-' . sanitize_title( $justify_content );
1046 1199 }
1047 1200
1048 - if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
1201 + $flex_wrap = $block['attrs']['layout']['flexWrap'] ?? null;
1202 + if ( ! empty( $flex_wrap ) && 'nowrap' === $flex_wrap ) {
1049 1203 $class_names[] = 'is-nowrap';
1050 1204 }
1051 1205
1052 1206 // Get classname for layout type.
1053 - if ( isset( $used_layout['type'] ) ) {
1207 + if ( isset( $used_layout['type'] ) && is_string( $used_layout['type'] ) ) {
1054 1208 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
1055 1209 } else {
1056 1210 $layout_classname = $layout_definitions['default']['className'] ?? '';
1057 1211 }
@@ -1067,9 +1221,11 @@
1067 1221 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
1068 1222
1069 1223 $gap_value = gutenberg_sanitize_block_gap_value( $block['attrs']['style']['spacing']['blockGap'] ?? null );
1070 1224
1071 - $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
1225 + $fallback_gap_value = gutenberg_sanitize_block_gap_value(
1226 + $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null
1227 + ) ?? '0.5em';
1072 1228 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
1073 1229
1074 1230 /*
1075 1231 * If a block's block.json skips serialization for spacing or spacing.blockGap,
@@ -1081,17 +1237,17 @@
1081 1237 $has_block_gap_support = isset( $block_gap );
1082 1238
1083 1239 // Get default blockGap value from global styles for use in layouts like grid.
1084 1240 // Check style variation first, then block-specific styles, then fall back to root styles.
1085 - $block_name = $block['blockName'] ?? '';
1086 - if ( null === $global_styles ) {
1087 - $global_styles = gutenberg_get_global_styles();
1088 - }
1241 + $block_name = $block['blockName'] ?? '';
1242 + $global_styles = gutenberg_get_global_styles();
1089 1243
1090 1244 // Check if the block has an active style variation with a blockGap value.
1091 1245 // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
1092 1246 $variation_block_gap_value = null;
1093 - $block_class_name = $block['attrs']['className'] ?? '';
1247 + $block_class_name = is_string( $block['attrs']['className'] ?? null )
1248 + ? $block['attrs']['className']
1249 + : '';
1094 1250 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1095 1251 $styles_registry = WP_Block_Styles_Registry::get_instance();
1096 1252 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1097 1253 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
@@ -1099,9 +1255,21 @@
1099 1255 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1100 1256 }
1101 1257 }
1102 1258
1103 - $global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;
1259 + $global_block_gap_candidates = array(
1260 + $variation_block_gap_value,
1261 + $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? null,
1262 + $global_styles['spacing']['blockGap'] ?? null,
1263 + );
1264 + $global_block_gap_value = null;
1265 + foreach ( $global_block_gap_candidates as $candidate_gap_value ) {
1266 + $candidate_gap_value = gutenberg_sanitize_block_gap_value( $candidate_gap_value );
1267 + if ( null !== $candidate_gap_value ) {
1268 + $global_block_gap_value = $candidate_gap_value;
1269 + break;
1270 + }
1271 + }
1104 1272
1105 1273 if ( null !== $global_block_gap_value ) {
1106 1274 $fallback_gap_value = $global_block_gap_value;
1107 1275 }
@@ -1121,9 +1289,9 @@
1121 1289 $fallback_gap_value,
1122 1290 $block_spacing,
1123 1291 );
1124 1292
1125 - foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
1293 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1126 1294 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1127 1295 if ( ! is_array( $viewport_style ) ) {
1128 1296 continue;
1129 1297 }
@@ -1167,9 +1335,9 @@
1167 1335 /*
1168 1336 * Emit responsive container layout styles using the same $container_class
1169 1337 * selector as the base layout so they target the inner block wrapper.
1170 1338 */
1171 - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1339 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1172 1340 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1173 1341 if ( ! is_array( $viewport_style ) ) {
1174 1342 continue;
1175 1343 }
@@ -1367,14 +1535,15 @@
1367 1535 * For themes without theme.json file, make sure
1368 1536 * to restore the inner div for the group block
1369 1537 * to avoid breaking styles relying on that div.
1370 1538 *
1371 - * @param string $block_content Rendered block content.
1372 - * @param array $block Block object.
1373 - * @return string Filtered block content.
1539 + * @param string $block_content Rendered block content.
1540 + * @param array $block Block object.
1541 + * @return string Filtered block content.
1374 1542 */
1375 1543 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1376 - $tag_name = $block['attrs']['tagName'] ?? 'div';
1544 + $tag_name_attr = $block['attrs']['tagName'] ?? null;
1545 + $tag_name = is_string( $tag_name_attr ) ? $tag_name_attr : 'div';
1377 1546 $group_with_inner_container_regex = sprintf(
1378 1547 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1379 1548 preg_quote( $tag_name, '/' )
1380 1549 );
@@ -1440,9 +1609,9 @@
1440 1609 * to restore the outer div for the aligned image block
1441 1610 * to avoid breaking styles relying on that div.
1442 1611 *
1443 1612 * @param string $block_content Rendered block content.
1444 - * @param array $block Block object.
1613 + * @param array $block Block object.
1445 1614 * @return string Filtered block content.
1446 1615 */
1447 1616 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1448 1617 if ( wp_theme_has_theme_json() ) {