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 +635 -243 23.2.2 → trunk View file →
@@ -237,26 +237,298 @@
237 237 }
238 238 }
239 239
240 240 /**
241 + * Returns the child-layout-only subset of a layout object.
242 + *
243 + * Child layout keys are the ones controlling how the block lays itself out
244 + * inside its parent's grid or flex container.
245 + *
246 + * @param mixed $layout Layout object.
247 + * @return array Child layout values, or an empty array.
248 + */
249 +function gutenberg_get_layout_child_values( $layout ) {
250 + if ( ! is_array( $layout ) ) {
251 + return array();
252 + }
253 +
254 + return array_intersect_key(
255 + $layout,
256 + array_flip(
257 + array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' )
258 + )
259 + );
260 +}
261 +
262 +/**
263 + * Returns the container-layout subset of a layout object (everything except child layout keys).
264 + *
265 + * @param mixed $layout Layout object.
266 + * @return array Container layout values, or an empty array.
267 + */
268 +function gutenberg_get_layout_container_values( $layout ) {
269 + if ( ! is_array( $layout ) ) {
270 + return array();
271 + }
272 +
273 + return array_diff_key(
274 + $layout,
275 + array_flip(
276 + array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' )
277 + )
278 + );
279 +}
280 +
281 +/**
282 + * Sanitizes a block gap value before layout style generation.
283 + *
284 + * Regex for CSS value borrowed from `safecss_filter_attr`, used here to only match
285 + * against the value, not the CSS attribute.
286 + *
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.
292 + */
293 +function gutenberg_sanitize_block_gap_value( $gap_value ) {
294 + if ( is_array( $gap_value ) ) {
295 + foreach ( $gap_value as $key => $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;
302 + }
303 + return empty( $gap_value ) ? null : $gap_value;
304 + }
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 +
318 + return $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
319 +}
320 +
321 +/**
322 + * Returns child layout styles for a block affected by its parent's layout.
323 + *
324 + * @param string $selector CSS selector.
325 + * @param array $child_layout Child layout values.
326 + * @param array $parent_layout Parent layout values.
327 + * @param array|null $viewport_overrides Optional. Child viewport layout overrides to emit.
328 + * @return array Child layout style rules.
329 + */
330 +function gutenberg_get_child_layout_style_rules( $selector, $child_layout, $parent_layout = array(), $viewport_overrides = null ) {
331 + $base_child_layout = is_array( $child_layout ) ? $child_layout : array();
332 + $viewport_overrides = is_array( $viewport_overrides ) ? $viewport_overrides : null;
333 + $child_layout = null === $viewport_overrides ? $base_child_layout : array_replace( $base_child_layout, $viewport_overrides );
334 + $child_layout_declarations = array();
335 + $child_layout_styles = array();
336 + $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
337 + return array_key_exists( $property, $viewport_overrides );
338 + };
339 +
340 + $self_stretch = $child_layout['selfStretch'] ?? null;
341 + $base_self_stretch = $base_child_layout['selfStretch'] ?? null;
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 +
358 + if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( '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'] ) ) {
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 + }
377 + $child_layout_declarations['box-sizing'] = 'border-box';
378 + } elseif ( $flex_child_layout_values['grow'] === $self_stretch ) {
379 + $child_layout_declarations['flex-grow'] = '1';
380 + }
381 + }
382 +
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;
394 + if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
395 + if ( $column_start && $column_span ) {
396 + $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
397 + } elseif ( $column_start ) {
398 + $child_layout_declarations['grid-column'] = "$column_start";
399 + } elseif ( $column_span ) {
400 + $child_layout_declarations['grid-column'] = "span $column_span";
401 + }
402 + }
403 +
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;
408 + if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
409 + if ( $row_start && $row_span ) {
410 + $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
411 + } elseif ( $row_start ) {
412 + $child_layout_declarations['grid-row'] = "$row_start";
413 + } elseif ( $row_span ) {
414 + $child_layout_declarations['grid-row'] = "span $row_span";
415 + }
416 + }
417 +
418 + if ( ! empty( $child_layout_declarations ) ) {
419 + $child_layout_styles[] = array(
420 + 'selector' => $selector,
421 + 'declarations' => $child_layout_declarations,
422 + );
423 + }
424 +
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;
428 +
429 + /*
430 + * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
431 + * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
432 + * once the grid has less columns than the start.
433 + * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
434 + * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
435 + */
436 + if ( null === $viewport_overrides && ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
437 + $column_span_number = floatval( $column_span );
438 + $column_start_number = floatval( $column_start );
439 + $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
440 + $parent_column_value = floatval( $parent_column_width );
441 + $parent_column_unit = explode( $parent_column_value, $parent_column_width );
442 +
443 + $num_cols_to_break_at = 2;
444 + if ( $column_span_number && $column_start_number ) {
445 + $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
446 + } elseif ( $column_span_number ) {
447 + $num_cols_to_break_at = $column_span_number;
448 + } else {
449 + $num_cols_to_break_at = $column_start_number;
450 + }
451 +
452 + /*
453 + * If there is no unit, the width has somehow been mangled so we reset both unit and value
454 + * to defaults.
455 + * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
456 + */
457 + if ( count( $parent_column_unit ) <= 1 ) {
458 + $parent_column_unit = 'rem';
459 + $parent_column_value = 12;
460 + } else {
461 + $parent_column_unit = $parent_column_unit[1];
462 +
463 + if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
464 + $parent_column_unit = 'rem';
465 + }
466 + }
467 +
468 + /*
469 + * A default gap value is used for this computation because custom gap values may not be
470 + * viable to use in the computation of the container query value.
471 + */
472 + $default_gap_value = 'px' === $parent_column_unit ? 24 : 1.5;
473 + $container_query_value = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
474 + $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
475 + $container_query_value = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
476 + // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
477 + $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
478 +
479 + $child_layout_styles[] = array(
480 + 'rules_group' => "@container (max-width: $container_query_value )",
481 + 'selector' => $selector,
482 + 'declarations' => array(
483 + 'grid-column' => $grid_column_value,
484 + 'grid-row' => 'auto',
485 + ),
486 + );
487 + }
488 +
489 + return $child_layout_styles;
490 +}
491 +
492 +/**
241 493 * Generates the CSS corresponding to the provided layout.
242 494 *
243 - * @param string $selector CSS selector.
244 - * @param array $layout Layout object. The one that is passed has already checked
245 - * the existence of default block layout.
246 - * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
247 - * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null.
248 - * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
249 - * @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'.
250 - * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
251 - * @return string CSS styles on success. Else, empty string.
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.
507 + * @return string CSS styles, or empty string.
252 508 */
253 -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 ) {
254 - $layout_type = $layout['type'] ?? 'default';
255 - $layout_styles = array();
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() ) {
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';
256 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.
524 + $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
525 + return array_key_exists( $property, $viewport_overrides );
526 + };
527 + $layout_styles = array();
528 +
257 529 if ( 'default' === $layout_type ) {
258 - if ( $has_block_gap_support ) {
530 + if ( $has_block_gap_support && $should_output_block_gap ) {
259 531 if ( is_array( $gap_value ) ) {
260 532 $gap_value = $gap_value['top'] ?? null;
261 533 }
262 534 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
@@ -286,15 +558,40 @@
286 558 );
287 559 }
288 560 }
289 561 } elseif ( 'constrained' === $layout_type ) {
290 - $content_size = $layout['contentSize'] ?? '';
291 - $wide_size = $layout['wideSize'] ?? '';
292 - $justify_content = $layout['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';
293 570
294 - $all_max_width_value = $content_size ? $content_size : $wide_size;
295 - $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' );
296 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 +
297 594 // Make sure there is a single CSS rule, and all tags are stripped for security.
298 595 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
299 596 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
300 597
@@ -300,18 +597,23 @@
300 597
301 598 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
302 599 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
303 600
304 - if ( $content_size || $wide_size ) {
601 + if ( $should_output_constrained_sizes && ( $content_size || $wide_size || $is_resetting_constrained_sizes ) ) {
602 + $content_size_declarations = array(
603 + 'max-width' => $all_max_width_value,
604 + );
605 +
606 + if ( null === $viewport_overrides || $has_justify_content_override ) {
607 + $content_size_declarations['margin-left'] = $margin_left;
608 + $content_size_declarations['margin-right'] = $margin_right;
609 + }
610 +
305 611 array_push(
306 612 $layout_styles,
307 613 array(
308 614 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
309 - 'declarations' => array(
310 - 'max-width' => $all_max_width_value,
311 - 'margin-left' => $margin_left,
312 - 'margin-right' => $margin_right,
313 - ),
615 + 'declarations' => $content_size_declarations,
314 616 ),
315 617 array(
316 618 'selector' => "$selector > .alignwide",
317 619 'declarations' => array( 'max-width' => $wide_max_width_value ),
@@ -322,9 +624,9 @@
322 624 )
323 625 );
324 626 }
325 627
326 - if ( isset( $block_spacing ) ) {
628 + if ( null === $viewport_overrides && isset( $block_spacing ) ) {
327 629 $block_spacing_values = gutenberg_style_engine_get_styles(
328 630 array(
329 631 'spacing' => $block_spacing,
330 632 )
@@ -357,23 +659,33 @@
357 659 );
358 660 }
359 661 }
360 662
361 - if ( 'left' === $justify_content ) {
663 + if ( $has_justify_content_override && ! $should_output_constrained_sizes ) {
362 664 $layout_styles[] = array(
363 665 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
364 - 'declarations' => array( 'margin-left' => '0 !important' ),
666 + 'declarations' => array(
667 + 'margin-left' => $margin_left,
668 + 'margin-right' => $margin_right,
669 + ),
365 670 );
366 - }
671 + } elseif ( null === $viewport_overrides ) {
672 + if ( 'left' === $justify_content ) {
673 + $layout_styles[] = array(
674 + 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
675 + 'declarations' => array( 'margin-left' => '0 !important' ),
676 + );
677 + }
367 678
368 - if ( 'right' === $justify_content ) {
369 - $layout_styles[] = array(
370 - 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
371 - 'declarations' => array( 'margin-right' => '0 !important' ),
372 - );
679 + if ( 'right' === $justify_content ) {
680 + $layout_styles[] = array(
681 + 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
682 + 'declarations' => array( 'margin-right' => '0 !important' ),
683 + );
684 + }
373 685 }
374 686
375 - if ( $has_block_gap_support ) {
687 + if ( $has_block_gap_support && $should_output_block_gap ) {
376 688 if ( is_array( $gap_value ) ) {
377 689 $gap_value = $gap_value['top'] ?? null;
378 690 }
379 691 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
@@ -403,9 +715,9 @@
403 715 );
404 716 }
405 717 }
406 718 } elseif ( 'flex' === $layout_type ) {
407 - $layout_orientation = $layout['orientation'] ?? 'horizontal';
719 + $layout_orientation = $layout_for_styles['orientation'] ?? 'horizontal';
408 720
409 721 $justify_content_options = array(
410 722 'left' => 'flex-start',
411 723 'right' => 'flex-end',
@@ -425,9 +737,17 @@
425 737 $justify_content_options += array( 'stretch' => 'stretch' );
426 738 $vertical_alignment_options += array( 'space-between' => 'space-between' );
427 739 }
428 740
429 - if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
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 + */
744 + $should_output_flex_wrap = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
745 + $should_output_flex_orientation = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
746 + $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
747 + $should_output_flex_alignment = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
748 +
749 + if ( $should_output_flex_wrap && ! empty( $layout_for_styles['flexWrap'] ) && 'nowrap' === $layout_for_styles['flexWrap'] ) {
430 750 $layout_styles[] = array(
431 751 'selector' => $selector,
432 752 'declarations' => array( 'flex-wrap' => 'nowrap' ),
433 753 );
@@ -432,9 +752,9 @@
432 752 'declarations' => array( 'flex-wrap' => 'nowrap' ),
433 753 );
434 754 }
435 755
436 - if ( $has_block_gap_support && isset( $gap_value ) ) {
756 + if ( $has_block_gap_support && $should_output_block_gap && isset( $gap_value ) ) {
437 757 $combined_gap_value = '';
438 758 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
439 759
440 760 foreach ( $gap_sides as $gap_side ) {
@@ -456,9 +776,9 @@
456 776 $combined_gap_value .= "$process_value ";
457 777 }
458 778 $gap_value = trim( $combined_gap_value );
459 779
460 - if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
780 + if ( '' !== $gap_value && ! $should_skip_gap_serialization ) {
461 781 $layout_styles[] = array(
462 782 'selector' => $selector,
463 783 'declarations' => array( 'gap' => $gap_value ),
464 784 );
@@ -464,53 +784,78 @@
464 784 );
465 785 }
466 786 }
467 787
788 + $flex_justify_content = $layout_for_styles['justifyContent'] ?? null;
789 + $flex_vertical_alignment = $layout_for_styles['verticalAlignment'] ?? null;
790 +
468 791 if ( 'horizontal' === $layout_orientation ) {
469 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 + /*
470 804 * Add this style only if is not empty for backwards compatibility,
471 805 * since we intend to convert blocks that had flex layout implemented
472 806 * by custom css.
473 807 */
474 - if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['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 ) ) {
475 809 $layout_styles[] = array(
476 810 'selector' => $selector,
477 - 'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ),
811 + 'declarations' => array( 'justify-content' => $justify_content_options[ $flex_justify_content ] ),
478 812 );
479 813 }
480 814
481 - if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['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 ) ) {
482 816 $layout_styles[] = array(
483 817 'selector' => $selector,
484 - 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
818 + 'declarations' => array( 'align-items' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
485 819 );
486 820 }
487 821 } else {
488 - $layout_styles[] = array(
489 - 'selector' => $selector,
490 - 'declarations' => array( 'flex-direction' => 'column' ),
491 - );
492 - if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
822 + if ( $should_output_flex_orientation ) {
493 823 $layout_styles[] = array(
494 824 'selector' => $selector,
495 - 'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ),
825 + 'declarations' => array( 'flex-direction' => 'column' ),
496 826 );
497 - } else {
827 + }
828 + if ( $should_output_flex_justification && ! empty( $flex_justify_content ) && is_string( $flex_justify_content ) && array_key_exists( $flex_justify_content, $justify_content_options ) ) {
498 829 $layout_styles[] = array(
499 830 'selector' => $selector,
831 + 'declarations' => array( 'align-items' => $justify_content_options[ $flex_justify_content ] ),
832 + );
833 + } elseif ( $should_output_flex_justification ) {
834 + $layout_styles[] = array(
835 + 'selector' => $selector,
500 836 'declarations' => array( 'align-items' => 'flex-start' ),
501 837 );
502 838 }
503 - if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['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 ) ) {
504 840 $layout_styles[] = array(
505 841 'selector' => $selector,
506 - 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
842 + 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $flex_vertical_alignment ] ),
507 843 );
508 844 }
509 845 }
510 846 } elseif ( 'grid' === $layout_type ) {
511 847 /*
512 - * 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
513 858 * is the relevant one for computation of responsive grid columns.
514 859 */
515 860 if ( is_array( $fallback_gap_value ) ) {
516 861 $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
@@ -537,12 +882,14 @@
537 882 $index_to_splice = strrpos( $process_value, '|' ) + 1;
538 883 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
539 884 $process_value = "var(--wp--preset--spacing--$slug)";
540 885 }
886 + if ( ! is_array( $gap_value ) || 'left' === $gap_side ) {
887 + $responsive_gap_value = $process_value;
888 + }
541 889 $combined_gap_value .= "$process_value ";
542 890 }
543 - $gap_value = trim( $combined_gap_value );
544 - $responsive_gap_value = $gap_value;
891 + $gap_value = trim( $combined_gap_value );
545 892 }
546 893
547 894 // Ensure 0 values have a unit so they work in calc().
548 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
@@ -548,47 +895,56 @@
548 895 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
549 896 $responsive_gap_value = '0px';
550 897 }
551 898
552 - if ( ! empty( $layout['columnCount'] ) && ! empty( $layout['minimumColumnWidth'] ) ) {
553 - $max_value = 'max(min(' . $layout['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout['columnCount'] . ' - 1))) /' . $layout['columnCount'] . ')';
554 - $layout_styles[] = array(
555 - 'selector' => $selector,
556 - 'declarations' => array(
557 - 'grid-template-columns' => 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))',
558 - 'container-type' => 'inline-size',
559 - ),
560 - );
561 - if ( ! empty( $layout['rowCount'] ) ) {
562 - $layout_styles[] = array(
563 - 'selector' => $selector,
564 - 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
565 - );
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'] );
904 + if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
905 + $should_output_grid_columns = true;
906 + }
907 +
908 + $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $column_count ) && ! empty( $row_count );
909 + $grid_declarations = array();
910 +
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))';
921 + } elseif ( $should_output_grid_columns ) {
922 + $minimum_column_width = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
923 + $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
924 + }
925 +
926 + if ( ! empty( $grid_declarations ) ) {
927 + $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
928 + if ( empty( $column_count ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
929 + if ( null === $viewport_overrides || ! $base_has_container_type ) {
930 + $grid_declarations['container-type'] = 'inline-size';
931 + }
566 932 }
567 - } elseif ( ! empty( $layout['columnCount'] ) ) {
568 933 $layout_styles[] = array(
569 934 'selector' => $selector,
570 - 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ),
935 + 'declarations' => $grid_declarations,
571 936 );
572 - if ( ! empty( $layout['rowCount'] ) ) {
573 - $layout_styles[] = array(
574 - 'selector' => $selector,
575 - 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
576 - );
577 - }
578 - } else {
579 - $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem';
937 + }
580 938
939 + if ( $should_output_grid_rows ) {
581 940 $layout_styles[] = array(
582 941 'selector' => $selector,
583 - 'declarations' => array(
584 - 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))',
585 - 'container-type' => 'inline-size',
586 - ),
942 + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $row_count . ', minmax(1rem, auto))' ),
587 943 );
588 944 }
589 945
590 - if ( $has_block_gap_support && null !== $gap_value && ! $should_skip_gap_serialization ) {
946 + if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
591 947 $layout_styles[] = array(
592 948 'selector' => $selector,
593 949 'declarations' => array( 'gap' => $gap_value ),
594 950 );
@@ -595,8 +951,14 @@
595 951 }
596 952 }
597 953
598 954 if ( ! empty( $layout_styles ) ) {
955 + if ( ! empty( $rules_group ) ) {
956 + foreach ( $layout_styles as $index => $layout_style ) {
957 + $layout_styles[ $index ]['rules_group'] = $rules_group;
958 + }
959 + }
960 +
599 961 /*
600 962 * Add to the style engine store to enqueue and render layout styles.
601 963 * Return compiled layout styles to retain backwards compatibility.
602 964 * Since https://github.com/WordPress/gutenberg/pull/42452,
@@ -622,10 +984,10 @@
622 984 * and increments on each call. The returned value is not universally unique,
623 985 * but it is unique across the life of the PHP process and it's stable per
624 986 * prefix.
625 987 *
626 - * @param string $prefix Prefix for the returned ID.
627 - * @return string Incremental ID per prefix.
988 + * @param string $prefix Prefix for the returned ID.
989 + * @return string Incremental ID per prefix.
628 990 */
629 991 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
630 992 static $id_counters = array();
631 993 if ( ! array_key_exists( $prefix, $id_counters ) ) {
@@ -654,21 +1016,54 @@
654 1016
655 1017 /**
656 1018 * Renders the layout config to the block wrapper.
657 1019 *
658 - * @param string $block_content Rendered block content.
659 - * @param array $block Block object.
660 - * @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.
661 1023 */
662 1024 function gutenberg_render_layout_support_flag( $block_content, $block ) {
663 - static $global_styles = null;
664 -
665 1025 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
666 1026 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
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 );
667 1049 // If there is any value in style -> layout, the block has a child layout.
668 - $child_layout = $block['attrs']['style']['layout'] ?? null;
1050 + $child_layout = $style_attr['layout'] ?? null;
669 1051
670 - if ( ! $block_supports_layout && ! $child_layout ) {
1052 + // Collect responsive viewport child layout overrides so that a block with
1053 + // only responsive child layout (no base child layout) is still processed.
1054 + $viewport_child_layouts = array();
1055 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1056 + $viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
1057 + if ( ! empty( $viewport_child ) ) {
1058 + $viewport_child_layouts[ $breakpoint ] = array(
1059 + 'media_query' => $media_query,
1060 + 'child_layout' => $viewport_child,
1061 + );
1062 + }
1063 + }
1064 +
1065 + if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
671 1066 return $block_content;
672 1067 }
673 1068
674 1069 $outer_class_names = array();
@@ -673,134 +1068,58 @@
673 1068
674 1069 $outer_class_names = array();
675 1070
676 1071 // Child layout specific logic.
677 - if ( $child_layout ) {
1072 + if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
1073 + $base_child_layout = gutenberg_get_layout_child_values( $child_layout );
1074 + $parent_layout = $block['parentLayout'] ?? array();
1075 +
678 1076 /*
679 1077 * Generates a unique class for child block layout styles.
680 1078 *
681 1079 * To ensure consistent class generation across different page renders,
682 1080 * only properties that affect layout styling are used. These properties
683 - * come from `$block['attrs']['style']['layout']` and `$block['parentLayout']`.
1081 + * come from `$block['attrs']['style']['layout']`, viewport overrides in
1082 + * `$block['attrs']['style'][$breakpoint]['layout']`, and
1083 + * `$block['parentLayout']`.
684 1084 *
685 1085 * As long as these properties coincide, the generated class will be the same.
686 1086 */
1087 + $container_content_hash_input = array(
1088 + 'layout' => $base_child_layout,
1089 + 'parentLayout' => array_intersect_key(
1090 + $parent_layout,
1091 + array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
1092 + ),
1093 + );
1094 + foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
1095 + $container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
1096 + }
687 1097 $container_content_class = gutenberg_unique_id_from_values(
688 - array(
689 - 'layout' => array_intersect_key(
690 - $block['attrs']['style']['layout'] ?? array(),
691 - array_flip(
692 - array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' )
693 - )
694 - ),
695 - 'parentLayout' => array_intersect_key(
696 - $block['parentLayout'] ?? array(),
697 - array_flip(
698 - array( 'minimumColumnWidth', 'columnCount' )
699 - )
700 - ),
701 - ),
1098 + $container_content_hash_input,
702 1099 'wp-container-content-'
703 1100 );
704 1101
705 - $child_layout_declarations = array();
706 - $child_layout_styles = array();
707 -
708 - $self_stretch = $block['attrs']['style']['layout']['selfStretch'] ?? null;
709 -
710 - if ( 'fixed' === $self_stretch && isset( $block['attrs']['style']['layout']['flexSize'] ) ) {
711 - $child_layout_declarations['flex-basis'] = $block['attrs']['style']['layout']['flexSize'];
712 - $child_layout_declarations['box-sizing'] = 'border-box';
713 - } elseif ( 'fill' === $self_stretch ) {
714 - $child_layout_declarations['flex-grow'] = '1';
715 - }
716 -
717 - $column_start = $block['attrs']['style']['layout']['columnStart'] ?? null;
718 - $column_span = $block['attrs']['style']['layout']['columnSpan'] ?? null;
719 - if ( $column_start && $column_span ) {
720 - $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
721 - } elseif ( $column_start ) {
722 - $child_layout_declarations['grid-column'] = "$column_start";
723 - } elseif ( $column_span ) {
724 - $child_layout_declarations['grid-column'] = "span $column_span";
725 - }
726 -
727 - $row_start = $block['attrs']['style']['layout']['rowStart'] ?? null;
728 - $row_span = $block['attrs']['style']['layout']['rowSpan'] ?? null;
729 - if ( $row_start && $row_span ) {
730 - $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
731 - } elseif ( $row_start ) {
732 - $child_layout_declarations['grid-row'] = "$row_start";
733 - } elseif ( $row_span ) {
734 - $child_layout_declarations['grid-row'] = "span $row_span";
735 - }
736 -
737 - $child_layout_styles[] = array(
738 - 'selector' => ".$container_content_class",
739 - 'declarations' => $child_layout_declarations,
1102 + $child_layout_styles = gutenberg_get_child_layout_style_rules(
1103 + ".$container_content_class",
1104 + $base_child_layout,
1105 + $parent_layout
740 1106 );
741 1107
742 - $minimum_column_width = $block['parentLayout']['minimumColumnWidth'] ?? null;
743 - $column_count = $block['parentLayout']['columnCount'] ?? null;
744 -
745 - /*
746 - * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
747 - * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
748 - * once the grid has less columns than the start.
749 - * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
750 - * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
751 - */
752 - if ( ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
753 - $column_span_number = floatval( $column_span );
754 - $column_start_number = floatval( $column_start );
755 - $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
756 - $parent_column_value = floatval( $parent_column_width );
757 - $parent_column_unit = explode( $parent_column_value, $parent_column_width );
758 -
759 - $num_cols_to_break_at = 2;
760 - if ( $column_span_number && $column_start_number ) {
761 - $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
762 - } elseif ( $column_span_number ) {
763 - $num_cols_to_break_at = $column_span_number;
764 - } else {
765 - $num_cols_to_break_at = $column_start_number;
1108 + // Emit responsive child layout CSS using the same container-content class
1109 + // so that base and responsive child layout share the exact same selector.
1110 + foreach ( $viewport_child_layouts as $viewport_data ) {
1111 + $viewport_child_styles = gutenberg_get_child_layout_style_rules(
1112 + ".$container_content_class",
1113 + $base_child_layout,
1114 + $parent_layout,
1115 + $viewport_data['child_layout']
1116 + );
1117 + foreach ( $viewport_child_styles as $index => $rule ) {
1118 + $viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
766 1119 }
767 1120
768 - /*
769 - * If there is no unit, the width has somehow been mangled so we reset both unit and value
770 - * to defaults.
771 - * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
772 - */
773 - if ( count( $parent_column_unit ) <= 1 ) {
774 - $parent_column_unit = 'rem';
775 - $parent_column_value = 12;
776 - } else {
777 - $parent_column_unit = $parent_column_unit[1];
778 -
779 - if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
780 - $parent_column_unit = 'rem';
781 - }
782 - }
783 -
784 - /*
785 - * A default gap value is used for this computation because custom gap values may not be
786 - * viable to use in the computation of the container query value.
787 - */
788 - $default_gap_value = 'px' === $parent_column_unit ? 24 : 1.5;
789 - $container_query_value = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
790 - $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
791 - $container_query_value = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
792 - // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
793 - $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
794 -
795 - $child_layout_styles[] = array(
796 - 'rules_group' => "@container (max-width: $container_query_value )",
797 - 'selector' => ".$container_content_class",
798 - 'declarations' => array(
799 - 'grid-column' => $grid_column_value,
800 - 'grid-row' => 'auto',
801 - ),
802 - );
1121 + $child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
803 1122 }
804 1123
805 1124 /*
806 1125 * Add to the style engine store to enqueue and render layout styles.
@@ -842,9 +1161,8 @@
842 1161 // Ensure layout classnames are not injected if there is no layout support.
843 1162 return $block_content;
844 1163 }
845 1164
846 - $global_settings = gutenberg_get_global_settings();
847 1165 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
848 1166 if ( empty( $fallback_layout ) ) {
849 1167 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
850 1168 }
@@ -869,22 +1187,25 @@
869 1187 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
870 1188 * not intended to provide an extended set of classes to match all block layout attributes
871 1189 * here.
872 1190 */
873 - if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
874 - $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 );
875 1194 }
876 1195
877 - if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
878 - $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 );
879 1199 }
880 1200
881 - 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 ) {
882 1203 $class_names[] = 'is-nowrap';
883 1204 }
884 1205
885 1206 // Get classname for layout type.
886 - if ( isset( $used_layout['type'] ) ) {
1207 + if ( isset( $used_layout['type'] ) && is_string( $used_layout['type'] ) ) {
887 1208 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
888 1209 } else {
889 1210 $layout_classname = $layout_definitions['default']['className'] ?? '';
890 1211 }
@@ -898,24 +1219,13 @@
898 1219 * Attribute-based Layout classnames are output in all cases.
899 1220 */
900 1221 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
901 1222
902 - $gap_value = $block['attrs']['style']['spacing']['blockGap'] ?? null;
1223 + $gap_value = gutenberg_sanitize_block_gap_value( $block['attrs']['style']['spacing']['blockGap'] ?? null );
903 1224
904 - /*
905 - * Skip if gap value contains unsupported characters.
906 - * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
907 - * to only match against the value, not the CSS attribute.
908 - */
909 - if ( is_array( $gap_value ) ) {
910 - foreach ( $gap_value as $key => $value ) {
911 - $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
912 - }
913 - } else {
914 - $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
915 - }
916 -
917 - $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';
918 1228 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
919 1229
920 1230 /*
921 1231 * If a block's block.json skips serialization for spacing or spacing.blockGap,
@@ -927,17 +1237,17 @@
927 1237 $has_block_gap_support = isset( $block_gap );
928 1238
929 1239 // Get default blockGap value from global styles for use in layouts like grid.
930 1240 // Check style variation first, then block-specific styles, then fall back to root styles.
931 - $block_name = $block['blockName'] ?? '';
932 - if ( null === $global_styles ) {
933 - $global_styles = gutenberg_get_global_styles();
934 - }
1241 + $block_name = $block['blockName'] ?? '';
1242 + $global_styles = gutenberg_get_global_styles();
935 1243
936 1244 // Check if the block has an active style variation with a blockGap value.
937 1245 // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
938 1246 $variation_block_gap_value = null;
939 - $block_class_name = $block['attrs']['className'] ?? '';
1247 + $block_class_name = is_string( $block['attrs']['className'] ?? null )
1248 + ? $block['attrs']['className']
1249 + : '';
940 1250 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
941 1251 $styles_registry = WP_Block_Styles_Registry::get_instance();
942 1252 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
943 1253 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
@@ -945,9 +1255,21 @@
945 1255 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
946 1256 }
947 1257 }
948 1258
949 - $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 + }
950 1272
951 1273 if ( null !== $global_block_gap_value ) {
952 1274 $fallback_gap_value = $global_block_gap_value;
953 1275 }
@@ -958,17 +1280,41 @@
958 1280 * even for different blocks with the same layout definition. We need this to
959 1281 * make the CSS class names stable across paginations for features like the
960 1282 * enhanced pagination of the Query block.
961 1283 */
1284 + $container_class_hash_input = array(
1285 + $used_layout,
1286 + $has_block_gap_support,
1287 + $gap_value,
1288 + $should_skip_gap_serialization,
1289 + $fallback_gap_value,
1290 + $block_spacing,
1291 + );
1292 +
1293 + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1294 + $viewport_style = $style_attr[ $breakpoint ] ?? null;
1295 + if ( ! is_array( $viewport_style ) ) {
1296 + continue;
1297 + }
1298 +
1299 + $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1300 + if ( ! empty( $viewport_container_layout ) ) {
1301 + $container_class_hash_input[] = array(
1302 + 'breakpoint' => $breakpoint,
1303 + 'layout' => $viewport_container_layout,
1304 + );
1305 + }
1306 +
1307 + if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
1308 + $container_class_hash_input[] = array(
1309 + 'breakpoint' => $breakpoint,
1310 + 'blockGap' => gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
1311 + );
1312 + }
1313 + }
1314 +
962 1315 $container_class = gutenberg_unique_id_from_values(
963 - array(
964 - $used_layout,
965 - $has_block_gap_support,
966 - $gap_value,
967 - $should_skip_gap_serialization,
968 - $fallback_gap_value,
969 - $block_spacing,
970 - ),
1316 + $container_class_hash_input,
971 1317 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
972 1318 );
973 1319
974 1320 $style = gutenberg_get_layout_style(
@@ -984,8 +1330,53 @@
984 1330 // Only add container class and enqueue block support styles if unique styles were generated.
985 1331 if ( ! empty( $style ) ) {
986 1332 $class_names[] = $container_class;
987 1333 }
1334 +
1335 + /*
1336 + * Emit responsive container layout styles using the same $container_class
1337 + * selector as the base layout so they target the inner block wrapper.
1338 + */
1339 + foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1340 + $viewport_style = $style_attr[ $breakpoint ] ?? null;
1341 + if ( ! is_array( $viewport_style ) ) {
1342 + continue;
1343 + }
1344 +
1345 + $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1346 + $has_viewport_layout = ! empty( $viewport_container_layout );
1347 + $has_viewport_block_gap = isset( $viewport_style['spacing']['blockGap'] );
1348 +
1349 + if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
1350 + continue;
1351 + }
1352 +
1353 + $viewport_gap_value = $has_viewport_block_gap
1354 + ? gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
1355 + : $gap_value;
1356 + $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
1357 + ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
1358 + : $block_spacing;
1359 +
1360 + $viewport_styles = gutenberg_get_layout_style(
1361 + ".$container_class",
1362 + $used_layout,
1363 + $has_block_gap_support,
1364 + $viewport_gap_value,
1365 + $should_skip_gap_serialization,
1366 + $fallback_gap_value,
1367 + $viewport_block_spacing,
1368 + array(
1369 + 'rules_group' => $media_query,
1370 + 'viewport_overrides' => $viewport_container_layout,
1371 + 'has_block_gap_override' => $has_viewport_block_gap,
1372 + )
1373 + );
1374 +
1375 + if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
1376 + $class_names[] = $container_class;
1377 + }
1378 + }
988 1379 }
989 1380
990 1381 // Add combined layout and block classname for global styles to hook onto.
991 1382 $split_block_name = explode( '/', $block['blockName'] );
@@ -1144,14 +1535,15 @@
1144 1535 * For themes without theme.json file, make sure
1145 1536 * to restore the inner div for the group block
1146 1537 * to avoid breaking styles relying on that div.
1147 1538 *
1148 - * @param string $block_content Rendered block content.
1149 - * @param array $block Block object.
1150 - * @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.
1151 1542 */
1152 1543 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1153 - $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';
1154 1546 $group_with_inner_container_regex = sprintf(
1155 1547 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1156 1548 preg_quote( $tag_name, '/' )
1157 1549 );
@@ -1217,9 +1609,9 @@
1217 1609 * to restore the outer div for the aligned image block
1218 1610 * to avoid breaking styles relying on that div.
1219 1611 *
1220 1612 * @param string $block_content Rendered block content.
1221 - * @param array $block Block object.
1613 + * @param array $block Block object.
1222 1614 * @return string Filtered block content.
1223 1615 */
1224 1616 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1225 1617 if ( wp_theme_has_theme_json() ) {