PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
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 → 24.1.0 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() ) {