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 +223 -89 23.5.1 → 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 ) {
@@ -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,17 +1237,17 @@
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;
1128 - $block_class_name = $block['attrs']['className'] ?? '';
1247 + $block_class_name = is_string( $block['attrs']['className'] ?? null )
1248 + ? $block['attrs']['className']
1249 + : '';
1129 1250 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1130 1251 $styles_registry = WP_Block_Styles_Registry::get_instance();
1131 1252 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1132 1253 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
@@ -1134,9 +1255,21 @@
1134 1255 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1135 1256 }
1136 1257 }
1137 1258
1138 - $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 + }
1139 1272
1140 1273 if ( null !== $global_block_gap_value ) {
1141 1274 $fallback_gap_value = $global_block_gap_value;
1142 1275 }
@@ -1156,9 +1289,9 @@
1156 1289 $fallback_gap_value,
1157 1290 $block_spacing,
1158 1291 );
1159 1292
1160 - foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
1293 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1161 1294 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1162 1295 if ( ! is_array( $viewport_style ) ) {
1163 1296 continue;
1164 1297 }
@@ -1202,9 +1335,9 @@
1202 1335 /*
1203 1336 * Emit responsive container layout styles using the same $container_class
1204 1337 * selector as the base layout so they target the inner block wrapper.
1205 1338 */
1206 - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1339 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1207 1340 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1208 1341 if ( ! is_array( $viewport_style ) ) {
1209 1342 continue;
1210 1343 }
@@ -1402,14 +1535,15 @@
1402 1535 * For themes without theme.json file, make sure
1403 1536 * to restore the inner div for the group block
1404 1537 * to avoid breaking styles relying on that div.
1405 1538 *
1406 - * @param string $block_content Rendered block content.
1407 - * @param array $block Block object.
1408 - * @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.
1409 1542 */
1410 1543 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1411 - $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';
1412 1546 $group_with_inner_container_regex = sprintf(
1413 1547 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1414 1548 preg_quote( $tag_name, '/' )
1415 1549 );
@@ -1475,9 +1609,9 @@
1475 1609 * to restore the outer div for the aligned image block
1476 1610 * to avoid breaking styles relying on that div.
1477 1611 *
1478 1612 * @param string $block_content Rendered block content.
1479 - * @param array $block Block object.
1613 + * @param array $block Block object.
1480 1614 * @return string Filtered block content.
1481 1615 */
1482 1616 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1483 1617 if ( wp_theme_has_theme_json() ) {