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 +220 -88 23.5.3 → 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 ) {
@@ -359,10 +379,19 @@
359 379 $child_layout_declarations['flex-grow'] = '1';
360 380 }
361 381 }
362 382
363 - $column_start = $child_layout['columnStart'] ?? null;
364 - $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;
365 394 if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
366 395 if ( $column_start && $column_span ) {
367 396 $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
368 397 } elseif ( $column_start ) {
@@ -371,10 +400,12 @@
371 400 $child_layout_declarations['grid-column'] = "span $column_span";
372 401 }
373 402 }
374 403
375 - $row_start = $child_layout['rowStart'] ?? null;
376 - $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;
377 408 if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
378 409 if ( $row_start && $row_span ) {
379 410 $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
380 411 } elseif ( $row_start ) {
@@ -390,10 +421,11 @@
390 421 'declarations' => $child_layout_declarations,
391 422 );
392 423 }
393 424
394 - $minimum_column_width = $parent_layout['minimumColumnWidth'] ?? null;
395 - $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;
396 428
397 429 /*
398 430 * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
399 431 * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
@@ -459,27 +491,37 @@
459 491
460 492 /**
461 493 * Generates the CSS corresponding to the provided layout.
462 494 *
463 - * @param string $selector CSS selector.
464 - * @param array $layout Layout object. The one that is passed has already checked
465 - * the existence of default block layout.
466 - * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
467 - * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null.
468 - * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
469 - * @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'.
470 - * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
471 - * @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.
472 507 * @return string CSS styles, or empty string.
473 508 */
474 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() ) {
475 - $base_layout = is_array( $layout ) ? $layout : array();
476 - $viewport_overrides = $options['viewport_overrides'] ?? null;
477 - $layout_for_styles = null === $viewport_overrides ? $base_layout : array_replace( $base_layout, $viewport_overrides );
478 - $layout_type = $base_layout['type'] ?? 'default';
479 - $rules_group = $options['rules_group'] ?? null;
480 - $has_block_gap_override = ! empty( $options['has_block_gap_override'] );
481 - $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.
482 524 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
483 525 return array_key_exists( $property, $viewport_overrides );
484 526 };
485 527 $layout_styles = array();
@@ -516,15 +558,40 @@
516 558 );
517 559 }
518 560 }
519 561 } elseif ( 'constrained' === $layout_type ) {
520 - $content_size = $layout_for_styles['contentSize'] ?? '';
521 - $wide_size = $layout_for_styles['wideSize'] ?? '';
522 - $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';
523 570
524 - $all_max_width_value = $content_size ? $content_size : $wide_size;
525 - $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' );
526 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 +
527 594 // Make sure there is a single CSS rule, and all tags are stripped for security.
528 595 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
529 596 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
530 597
@@ -530,11 +597,9 @@
530 597
531 598 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
532 599 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
533 600
534 - $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
535 - $should_output_constrained_sizes = null === $viewport_overrides || $has_viewport_property_override( 'contentSize' ) || $has_viewport_property_override( 'wideSize' );
536 - if ( $should_output_constrained_sizes && ( $content_size || $wide_size ) ) {
601 + if ( $should_output_constrained_sizes && ( $content_size || $wide_size || $is_resetting_constrained_sizes ) ) {
537 602 $content_size_declarations = array(
538 603 'max-width' => $all_max_width_value,
539 604 );
540 605
@@ -672,8 +737,11 @@
672 737 $justify_content_options += array( 'stretch' => 'stretch' );
673 738 $vertical_alignment_options += array( 'space-between' => 'space-between' );
674 739 }
675 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 + */
676 744 $should_output_flex_wrap = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
677 745 $should_output_flex_orientation = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
678 746 $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
679 747 $should_output_flex_alignment = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
@@ -708,9 +776,9 @@
708 776 $combined_gap_value .= "$process_value ";
709 777 }
710 778 $gap_value = trim( $combined_gap_value );
711 779
712 - if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
780 + if ( '' !== $gap_value && ! $should_skip_gap_serialization ) {
713 781 $layout_styles[] = array(
714 782 'selector' => $selector,
715 783 'declarations' => array( 'gap' => $gap_value ),
716 784 );
@@ -716,25 +784,39 @@
716 784 );
717 785 }
718 786 }
719 787
788 + $flex_justify_content = $layout_for_styles['justifyContent'] ?? null;
789 + $flex_vertical_alignment = $layout_for_styles['verticalAlignment'] ?? null;
790 +
720 791 if ( 'horizontal' === $layout_orientation ) {
721 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 + /*
722 804 * Add this style only if is not empty for backwards compatibility,
723 805 * since we intend to convert blocks that had flex layout implemented
724 806 * by custom css.
725 807 */
726 - 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 ) ) {
727 809 $layout_styles[] = array(
728 810 'selector' => $selector,
729 - 'declarations' => array( 'justify-content' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
811 + 'declarations' => array( 'justify-content' => $justify_content_options[ $flex_justify_content ] ),
730 812 );
731 813 }
732 814
733 - 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 ) ) {
734 816 $layout_styles[] = array(
735 817 'selector' => $selector,
736 - 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
818 + 'declarations' => array( 'align-items' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
737 819 );
738 820 }
739 821 } else {
740 822 if ( $should_output_flex_orientation ) {
@@ -742,12 +824,12 @@
742 824 'selector' => $selector,
743 825 'declarations' => array( 'flex-direction' => 'column' ),
744 826 );
745 827 }
746 - 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 ) ) {
747 829 $layout_styles[] = array(
748 830 'selector' => $selector,
749 - 'declarations' => array( 'align-items' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
831 + 'declarations' => array( 'align-items' => $justify_content_options[ $flex_justify_content ] ),
750 832 );
751 833 } elseif ( $should_output_flex_justification ) {
752 834 $layout_styles[] = array(
753 835 'selector' => $selector,
@@ -753,18 +835,27 @@
753 835 'selector' => $selector,
754 836 'declarations' => array( 'align-items' => 'flex-start' ),
755 837 );
756 838 }
757 - 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 ) ) {
758 840 $layout_styles[] = array(
759 841 'selector' => $selector,
760 - 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
842 + 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
761 843 );
762 844 }
763 845 }
764 846 } elseif ( 'grid' === $layout_type ) {
765 847 /*
766 - * 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
767 858 * is the relevant one for computation of responsive grid columns.
768 859 */
769 860 if ( is_array( $fallback_gap_value ) ) {
770 861 $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
@@ -791,12 +882,14 @@
791 882 $index_to_splice = strrpos( $process_value, '|' ) + 1;
792 883 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
793 884 $process_value = "var(--wp--preset--spacing--$slug)";
794 885 }
886 + if ( ! is_array( $gap_value ) || 'left' === $gap_side ) {
887 + $responsive_gap_value = $process_value;
888 + }
795 889 $combined_gap_value .= "$process_value ";
796 890 }
797 - $gap_value = trim( $combined_gap_value );
798 - $responsive_gap_value = $gap_value;
891 + $gap_value = trim( $combined_gap_value );
799 892 }
800 893
801 894 // Ensure 0 values have a unit so they work in calc().
802 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
@@ -802,30 +895,38 @@
802 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
803 896 $responsive_gap_value = '0px';
804 897 }
805 898
806 - $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' );
807 - $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'] );
808 904 if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
809 905 $should_output_grid_columns = true;
810 906 }
811 907
812 - $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 );
813 909 $grid_declarations = array();
814 910
815 - if ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
816 - $max_value = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout_for_styles['columnCount'] . ' - 1))) /' . $layout_for_styles['columnCount'] . ')';
817 - $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))';
818 - } elseif ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) ) {
819 - $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))';
820 921 } elseif ( $should_output_grid_columns ) {
821 922 $minimum_column_width = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
822 - $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))';
823 924 }
824 925
825 926 if ( ! empty( $grid_declarations ) ) {
826 927 $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
827 - if ( empty( $layout_for_styles['columnCount'] ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
928 + if ( empty( $column_count ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
828 929 if ( null === $viewport_overrides || ! $base_has_container_type ) {
829 930 $grid_declarations['container-type'] = 'inline-size';
830 931 }
831 932 }
@@ -837,9 +938,9 @@
837 938
838 939 if ( $should_output_grid_rows ) {
839 940 $layout_styles[] = array(
840 941 'selector' => $selector,
841 - '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))' ),
842 943 );
843 944 }
844 945
845 946 if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
@@ -883,10 +984,10 @@
883 984 * and increments on each call. The returned value is not universally unique,
884 985 * but it is unique across the life of the PHP process and it's stable per
885 986 * prefix.
886 987 *
887 - * @param string $prefix Prefix for the returned ID.
888 - * @return string Incremental ID per prefix.
988 + * @param string $prefix Prefix for the returned ID.
989 + * @return string Incremental ID per prefix.
889 990 */
890 991 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
891 992 static $id_counters = array();
892 993 if ( ! array_key_exists( $prefix, $id_counters ) ) {
@@ -915,15 +1016,13 @@
915 1016
916 1017 /**
917 1018 * Renders the layout config to the block wrapper.
918 1019 *
919 - * @param string $block_content Rendered block content.
920 - * @param array $block Block object.
921 - * @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.
922 1023 */
923 1024 function gutenberg_render_layout_support_flag( $block_content, $block ) {
924 - static $global_styles = null;
925 -
926 1025 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
927 1026 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
928 1027 $style_attr = gutenberg_resolve_style_state_aliases(
929 1028 $block['attrs']['style'] ?? array(),
@@ -928,8 +1027,26 @@
928 1027 $style_attr = gutenberg_resolve_style_state_aliases(
929 1028 $block['attrs']['style'] ?? array(),
930 1029 $block['blockName']
931 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 );
932 1049 // If there is any value in style -> layout, the block has a child layout.
933 1050 $child_layout = $style_attr['layout'] ?? null;
934 1051
935 1052 // Collect responsive viewport child layout overrides so that a block with
@@ -934,9 +1051,9 @@
934 1051
935 1052 // Collect responsive viewport child layout overrides so that a block with
936 1053 // only responsive child layout (no base child layout) is still processed.
937 1054 $viewport_child_layouts = array();
938 - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1055 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
939 1056 $viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
940 1057 if ( ! empty( $viewport_child ) ) {
941 1058 $viewport_child_layouts[ $breakpoint ] = array(
942 1059 'media_query' => $media_query,
@@ -1044,9 +1161,8 @@
1044 1161 // Ensure layout classnames are not injected if there is no layout support.
1045 1162 return $block_content;
1046 1163 }
1047 1164
1048 - $global_settings = gutenberg_get_global_settings();
1049 1165 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
1050 1166 if ( empty( $fallback_layout ) ) {
1051 1167 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
1052 1168 }
@@ -1071,22 +1187,25 @@
1071 1187 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
1072 1188 * not intended to provide an extended set of classes to match all block layout attributes
1073 1189 * here.
1074 1190 */
1075 - if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
1076 - $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 );
1077 1194 }
1078 1195
1079 - if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
1080 - $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 );
1081 1199 }
1082 1200
1083 - 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 ) {
1084 1203 $class_names[] = 'is-nowrap';
1085 1204 }
1086 1205
1087 1206 // Get classname for layout type.
1088 - if ( isset( $used_layout['type'] ) ) {
1207 + if ( isset( $used_layout['type'] ) && is_string( $used_layout['type'] ) ) {
1089 1208 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
1090 1209 } else {
1091 1210 $layout_classname = $layout_definitions['default']['className'] ?? '';
1092 1211 }
@@ -1102,9 +1221,11 @@
1102 1221 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
1103 1222
1104 1223 $gap_value = gutenberg_sanitize_block_gap_value( $block['attrs']['style']['spacing']['blockGap'] ?? null );
1105 1224
1106 - $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';
1107 1228 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
1108 1229
1109 1230 /*
1110 1231 * If a block's block.json skips serialization for spacing or spacing.blockGap,
@@ -1116,12 +1237,10 @@
1116 1237 $has_block_gap_support = isset( $block_gap );
1117 1238
1118 1239 // Get default blockGap value from global styles for use in layouts like grid.
1119 1240 // Check style variation first, then block-specific styles, then fall back to root styles.
1120 - $block_name = $block['blockName'] ?? '';
1121 - if ( null === $global_styles ) {
1122 - $global_styles = gutenberg_get_global_styles();
1123 - }
1241 + $block_name = $block['blockName'] ?? '';
1242 + $global_styles = gutenberg_get_global_styles();
1124 1243
1125 1244 // Check if the block has an active style variation with a blockGap value.
1126 1245 // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
1127 1246 $variation_block_gap_value = null;
@@ -1136,9 +1255,21 @@
1136 1255 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1137 1256 }
1138 1257 }
1139 1258
1140 - $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 + }
1141 1272
1142 1273 if ( null !== $global_block_gap_value ) {
1143 1274 $fallback_gap_value = $global_block_gap_value;
1144 1275 }
@@ -1158,9 +1289,9 @@
1158 1289 $fallback_gap_value,
1159 1290 $block_spacing,
1160 1291 );
1161 1292
1162 - foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
1293 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1163 1294 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1164 1295 if ( ! is_array( $viewport_style ) ) {
1165 1296 continue;
1166 1297 }
@@ -1204,9 +1335,9 @@
1204 1335 /*
1205 1336 * Emit responsive container layout styles using the same $container_class
1206 1337 * selector as the base layout so they target the inner block wrapper.
1207 1338 */
1208 - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1339 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1209 1340 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1210 1341 if ( ! is_array( $viewport_style ) ) {
1211 1342 continue;
1212 1343 }
@@ -1404,14 +1535,15 @@
1404 1535 * For themes without theme.json file, make sure
1405 1536 * to restore the inner div for the group block
1406 1537 * to avoid breaking styles relying on that div.
1407 1538 *
1408 - * @param string $block_content Rendered block content.
1409 - * @param array $block Block object.
1410 - * @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.
1411 1542 */
1412 1543 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1413 - $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';
1414 1546 $group_with_inner_container_regex = sprintf(
1415 1547 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1416 1548 preg_quote( $tag_name, '/' )
1417 1549 );
@@ -1477,9 +1609,9 @@
1477 1609 * to restore the outer div for the aligned image block
1478 1610 * to avoid breaking styles relying on that div.
1479 1611 *
1480 1612 * @param string $block_content Rendered block content.
1481 - * @param array $block Block object.
1613 + * @param array $block Block object.
1482 1614 * @return string Filtered block content.
1483 1615 */
1484 1616 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1485 1617 if ( wp_theme_has_theme_json() ) {