PluginProbe
Gutenberg / 23.6.0
Gutenberg v23.6.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 12.6.0 7.4.0 All 402 releases
gutenberg / lib / block-supports / layout.php

layout.php in Gutenberg 23.6.0, at lib/block-supports/layout.php

1,572 lines 59.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Layout block support flag.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Get the first style variation name from a className string that matches a registered style.
10 *
11 * @param string $class_name CSS class string for a block.
12 * @param array<string, array<string, mixed>> $registered_styles Currently registered block styles.
13 * @return string|null The name of the first registered variation, or null if none found.
14 */
15 function gutenberg_get_block_style_variation_name_from_registered_style( string $class_name, array $registered_styles = array() ): ?string {
16 if ( ! $class_name ) {
17 return null;
18 }
19
20 $registered_names = array_filter( array_column( $registered_styles, 'name' ) );
21
22 $prefix = 'is-style-';
23 $length = strlen( $prefix );
24
25 foreach ( explode( ' ', $class_name ) as $class ) {
26 if ( str_starts_with( $class, $prefix ) ) {
27 $variation = substr( $class, $length );
28 if ( 'default' !== $variation && in_array( $variation, $registered_names, true ) ) {
29 return $variation;
30 }
31 }
32 }
33
34 return null;
35 }
36
37 /**
38 * Returns layout definitions, keyed by layout type.
39 *
40 * Provides a common definition of slugs, classnames, base styles, and spacing styles for each layout type.
41 * When making changes or additions to layout definitions, the corresponding JavaScript definitions should
42 * also be updated.
43 *
44 * @return array[] Layout definitions.
45 */
46 function gutenberg_get_layout_definitions() {
47 $layout_definitions = array(
48 'default' => array(
49 'name' => 'default',
50 'slug' => 'flow',
51 'className' => 'is-layout-flow',
52 'baseStyles' => array(
53 array(
54 'selector' => ' > .alignleft',
55 'rules' => array(
56 'float' => 'left',
57 'margin-inline-start' => '0',
58 'margin-inline-end' => '2em',
59 ),
60 ),
61 array(
62 'selector' => ' > .alignright',
63 'rules' => array(
64 'float' => 'right',
65 'margin-inline-start' => '2em',
66 'margin-inline-end' => '0',
67 ),
68 ),
69 array(
70 'selector' => ' > .aligncenter',
71 'rules' => array(
72 'margin-left' => 'auto !important',
73 'margin-right' => 'auto !important',
74 ),
75 ),
76 ),
77 'spacingStyles' => array(
78 array(
79 'selector' => ' > :first-child',
80 'rules' => array(
81 'margin-block-start' => '0',
82 ),
83 ),
84 array(
85 'selector' => ' > :last-child',
86 'rules' => array(
87 'margin-block-end' => '0',
88 ),
89 ),
90 array(
91 'selector' => ' > *',
92 'rules' => array(
93 'margin-block-start' => null,
94 'margin-block-end' => '0',
95 ),
96 ),
97 ),
98 ),
99 'constrained' => array(
100 'name' => 'constrained',
101 'slug' => 'constrained',
102 'className' => 'is-layout-constrained',
103 'baseStyles' => array(
104 array(
105 'selector' => ' > .alignleft',
106 'rules' => array(
107 'float' => 'left',
108 'margin-inline-start' => '0',
109 'margin-inline-end' => '2em',
110 ),
111 ),
112 array(
113 'selector' => ' > .alignright',
114 'rules' => array(
115 'float' => 'right',
116 'margin-inline-start' => '2em',
117 'margin-inline-end' => '0',
118 ),
119 ),
120 array(
121 'selector' => ' > .aligncenter',
122 'rules' => array(
123 'margin-left' => 'auto !important',
124 'margin-right' => 'auto !important',
125 ),
126 ),
127 array(
128 'selector' => ' > :where(:not(.alignleft):not(.alignright):not(.alignfull))',
129 'rules' => array(
130 'max-width' => 'var(--wp--style--global--content-size)',
131 'margin-left' => 'auto !important',
132 'margin-right' => 'auto !important',
133 ),
134 ),
135 array(
136 'selector' => ' > .alignwide',
137 'rules' => array(
138 'max-width' => 'var(--wp--style--global--wide-size)',
139 ),
140 ),
141 ),
142 'spacingStyles' => array(
143 array(
144 'selector' => ' > :first-child',
145 'rules' => array(
146 'margin-block-start' => '0',
147 ),
148 ),
149 array(
150 'selector' => ' > :last-child',
151 'rules' => array(
152 'margin-block-end' => '0',
153 ),
154 ),
155 array(
156 'selector' => ' > *',
157 'rules' => array(
158 'margin-block-start' => null,
159 'margin-block-end' => '0',
160 ),
161 ),
162 ),
163 ),
164 'flex' => array(
165 'name' => 'flex',
166 'slug' => 'flex',
167 'className' => 'is-layout-flex',
168 'displayMode' => 'flex',
169 'baseStyles' => array(
170 array(
171 'selector' => '',
172 'rules' => array(
173 'flex-wrap' => 'wrap',
174 'align-items' => 'center',
175 ),
176 ),
177 array(
178 'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
179 'rules' => array(
180 'margin' => '0',
181 ),
182 ),
183 ),
184 'spacingStyles' => array(
185 array(
186 'selector' => '',
187 'rules' => array(
188 'gap' => null,
189 ),
190 ),
191 ),
192 ),
193 'grid' => array(
194 'name' => 'grid',
195 'slug' => 'grid',
196 'className' => 'is-layout-grid',
197 'displayMode' => 'grid',
198 'baseStyles' => array(
199 array(
200 'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
201 'rules' => array(
202 'margin' => '0',
203 ),
204 ),
205 ),
206 'spacingStyles' => array(
207 array(
208 'selector' => '',
209 'rules' => array(
210 'gap' => null,
211 ),
212 ),
213 ),
214 ),
215 );
216
217 return $layout_definitions;
218 }
219
220 /**
221 * Registers the layout block attribute for block types that support it.
222 *
223 * @param WP_Block_Type $block_type Block Type.
224 */
225 function gutenberg_register_layout_support( $block_type ) {
226 $support_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
227 if ( $support_layout ) {
228 if ( ! $block_type->attributes ) {
229 $block_type->attributes = array();
230 }
231
232 if ( ! array_key_exists( 'layout', $block_type->attributes ) ) {
233 $block_type->attributes['layout'] = array(
234 'type' => 'object',
235 );
236 }
237 }
238 }
239
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 * @param string|array|null $gap_value Block gap value.
288 * @return string|array|null Sanitized block gap value.
289 */
290 function gutenberg_sanitize_block_gap_value( $gap_value ) {
291 if ( is_array( $gap_value ) ) {
292 foreach ( $gap_value as $key => $value ) {
293 $gap_value[ $key ] = ! is_scalar( $value ) || ( $value && preg_match( '%[\\\(&=}]|/\*%', (string) $value ) ) ? null : $value;
294 }
295 return $gap_value;
296 }
297
298 return $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
299 }
300
301 /**
302 * Returns child layout styles for a block affected by its parent's layout.
303 *
304 * @param string $selector CSS selector.
305 * @param array $child_layout Child layout values.
306 * @param array $parent_layout Parent layout values.
307 * @param array|null $viewport_overrides Optional. Child viewport layout overrides to emit.
308 * @return array Child layout style rules.
309 */
310 function gutenberg_get_child_layout_style_rules( $selector, $child_layout, $parent_layout = array(), $viewport_overrides = null ) {
311 $base_child_layout = is_array( $child_layout ) ? $child_layout : array();
312 $viewport_overrides = is_array( $viewport_overrides ) ? $viewport_overrides : null;
313 $child_layout = null === $viewport_overrides ? $base_child_layout : array_replace( $base_child_layout, $viewport_overrides );
314 $child_layout_declarations = array();
315 $child_layout_styles = array();
316 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
317 return array_key_exists( $property, $viewport_overrides );
318 };
319
320 $self_stretch = $child_layout['selfStretch'] ?? null;
321 $base_self_stretch = $base_child_layout['selfStretch'] ?? null;
322
323 /*
324 * These are the serialized `selfStretch` values. `max` used to be called
325 * "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`.
326 */
327 $flex_child_layout_values = array(
328 'fit' => 'fit',
329 'grow' => 'fill',
330 'max' => 'fixed',
331 'fixed' => 'fixedNoShrink',
332 );
333 $flex_size_values = array(
334 $flex_child_layout_values['max'],
335 $flex_child_layout_values['fixed'],
336 );
337
338 if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( 'flexSize' ) ) {
339 if (
340 null !== $viewport_overrides &&
341 ( $flex_child_layout_values['fit'] === $self_stretch || $flex_child_layout_values['grow'] === $self_stretch ) &&
342 in_array( $base_self_stretch, $flex_size_values, true ) &&
343 isset( $base_child_layout['flexSize'] )
344 ) {
345 $child_layout_declarations['flex-basis'] = 'unset';
346 if ( $flex_child_layout_values['fixed'] === $base_self_stretch ) {
347 $child_layout_declarations['flex-shrink'] = 'unset';
348 }
349 }
350 if ( in_array( $self_stretch, $flex_size_values, true ) && isset( $child_layout['flexSize'] ) ) {
351 $child_layout_declarations['flex-basis'] = $child_layout['flexSize'];
352 if ( $flex_child_layout_values['fixed'] === $self_stretch ) {
353 $child_layout_declarations['flex-shrink'] = '0';
354 } elseif ( null !== $viewport_overrides && $flex_child_layout_values['fixed'] === $base_self_stretch ) {
355 $child_layout_declarations['flex-shrink'] = 'unset';
356 }
357 $child_layout_declarations['box-sizing'] = 'border-box';
358 } elseif ( $flex_child_layout_values['grow'] === $self_stretch ) {
359 $child_layout_declarations['flex-grow'] = '1';
360 }
361 }
362
363 $column_start = $child_layout['columnStart'] ?? null;
364 $column_span = $child_layout['columnSpan'] ?? null;
365 if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
366 if ( $column_start && $column_span ) {
367 $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
368 } elseif ( $column_start ) {
369 $child_layout_declarations['grid-column'] = "$column_start";
370 } elseif ( $column_span ) {
371 $child_layout_declarations['grid-column'] = "span $column_span";
372 }
373 }
374
375 $row_start = $child_layout['rowStart'] ?? null;
376 $row_span = $child_layout['rowSpan'] ?? null;
377 if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
378 if ( $row_start && $row_span ) {
379 $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
380 } elseif ( $row_start ) {
381 $child_layout_declarations['grid-row'] = "$row_start";
382 } elseif ( $row_span ) {
383 $child_layout_declarations['grid-row'] = "span $row_span";
384 }
385 }
386
387 if ( ! empty( $child_layout_declarations ) ) {
388 $child_layout_styles[] = array(
389 'selector' => $selector,
390 'declarations' => $child_layout_declarations,
391 );
392 }
393
394 $minimum_column_width = $parent_layout['minimumColumnWidth'] ?? null;
395 $column_count = $parent_layout['columnCount'] ?? null;
396
397 /*
398 * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
399 * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
400 * once the grid has less columns than the start.
401 * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
402 * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
403 */
404 if ( null === $viewport_overrides && ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
405 $column_span_number = floatval( $column_span );
406 $column_start_number = floatval( $column_start );
407 $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
408 $parent_column_value = floatval( $parent_column_width );
409 $parent_column_unit = explode( $parent_column_value, $parent_column_width );
410
411 $num_cols_to_break_at = 2;
412 if ( $column_span_number && $column_start_number ) {
413 $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
414 } elseif ( $column_span_number ) {
415 $num_cols_to_break_at = $column_span_number;
416 } else {
417 $num_cols_to_break_at = $column_start_number;
418 }
419
420 /*
421 * If there is no unit, the width has somehow been mangled so we reset both unit and value
422 * to defaults.
423 * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
424 */
425 if ( count( $parent_column_unit ) <= 1 ) {
426 $parent_column_unit = 'rem';
427 $parent_column_value = 12;
428 } else {
429 $parent_column_unit = $parent_column_unit[1];
430
431 if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
432 $parent_column_unit = 'rem';
433 }
434 }
435
436 /*
437 * A default gap value is used for this computation because custom gap values may not be
438 * viable to use in the computation of the container query value.
439 */
440 $default_gap_value = 'px' === $parent_column_unit ? 24 : 1.5;
441 $container_query_value = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
442 $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
443 $container_query_value = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
444 // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
445 $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
446
447 $child_layout_styles[] = array(
448 'rules_group' => "@container (max-width: $container_query_value )",
449 'selector' => $selector,
450 'declarations' => array(
451 'grid-column' => $grid_column_value,
452 'grid-row' => 'auto',
453 ),
454 );
455 }
456
457 return $child_layout_styles;
458 }
459
460 /**
461 * Generates the CSS corresponding to the provided layout.
462 *
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.
472 * @return string CSS styles, or empty string.
473 */
474 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;
482 // Viewport styles only store changed fields. If a field is present with null,
483 // the user cleared a value inherited from the default viewport, so check
484 // whether the key exists rather than whether the value is truthy.
485 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
486 return array_key_exists( $property, $viewport_overrides );
487 };
488 $layout_styles = array();
489
490 if ( 'default' === $layout_type ) {
491 if ( $has_block_gap_support && $should_output_block_gap ) {
492 if ( is_array( $gap_value ) ) {
493 $gap_value = $gap_value['top'] ?? null;
494 }
495 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
496 // Get spacing CSS variable from preset value if provided.
497 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
498 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
499 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
500 $gap_value = "var(--wp--preset--spacing--$slug)";
501 }
502
503 array_push(
504 $layout_styles,
505 array(
506 'selector' => "$selector > *",
507 'declarations' => array(
508 'margin-block-start' => '0',
509 'margin-block-end' => '0',
510 ),
511 ),
512 array(
513 'selector' => "$selector > * + *",
514 'declarations' => array(
515 'margin-block-start' => $gap_value,
516 'margin-block-end' => '0',
517 ),
518 )
519 );
520 }
521 }
522 } elseif ( 'constrained' === $layout_type ) {
523 $content_size = $layout_for_styles['contentSize'] ?? '';
524 $wide_size = $layout_for_styles['wideSize'] ?? '';
525 $justify_content = $layout_for_styles['justifyContent'] ?? 'center';
526
527 // Check if viewport-specific ("override") values exist. Null values are valid and mean the user cleared a value inherited from the default viewport.
528 $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
529 $has_content_size_override = null !== $viewport_overrides && $has_viewport_property_override( 'contentSize' );
530 $has_wide_size_override = null !== $viewport_overrides && $has_viewport_property_override( 'wideSize' );
531
532 /* 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
533 * value for contentSize or wideSize. If a viewport clears a custom constrained size, reset to the global layout variable.
534 */
535 $should_output_constrained_sizes = null === $viewport_overrides || $has_content_size_override || $has_wide_size_override;
536 $is_resetting_constrained_sizes = null !== $viewport_overrides &&
537 (
538 ( $has_content_size_override && ! $content_size ) ||
539 ( $has_wide_size_override && ! $wide_size )
540 );
541
542 // If a viewport clears a custom constrained size, reset to the global layout variable.
543 $all_max_width_value = $content_size
544 ? $content_size
545 : ( $wide_size && ! $has_content_size_override ? $wide_size : 'var(--wp--style--global--content-size, none)' );
546 $wide_max_width_value = $wide_size
547 ? $wide_size
548 : ( $content_size && ! $has_wide_size_override ? $content_size : 'var(--wp--style--global--wide-size, none)' );
549
550 // Make sure there is a single CSS rule, and all tags are stripped for security.
551 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
552 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
553
554 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
555 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
556
557 if ( $should_output_constrained_sizes && ( $content_size || $wide_size || $is_resetting_constrained_sizes ) ) {
558 $content_size_declarations = array(
559 'max-width' => $all_max_width_value,
560 );
561
562 if ( null === $viewport_overrides || $has_justify_content_override ) {
563 $content_size_declarations['margin-left'] = $margin_left;
564 $content_size_declarations['margin-right'] = $margin_right;
565 }
566
567 array_push(
568 $layout_styles,
569 array(
570 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
571 'declarations' => $content_size_declarations,
572 ),
573 array(
574 'selector' => "$selector > .alignwide",
575 'declarations' => array( 'max-width' => $wide_max_width_value ),
576 ),
577 array(
578 'selector' => "$selector .alignfull",
579 'declarations' => array( 'max-width' => 'none' ),
580 )
581 );
582 }
583
584 if ( null === $viewport_overrides && isset( $block_spacing ) ) {
585 $block_spacing_values = gutenberg_style_engine_get_styles(
586 array(
587 'spacing' => $block_spacing,
588 )
589 );
590
591 /*
592 * Handle negative margins for alignfull children of blocks with custom padding set.
593 * They're added separately because padding might only be set on one side.
594 */
595 if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
596 $padding_right = $block_spacing_values['declarations']['padding-right'];
597 // Add unit if 0.
598 if ( '0' === $padding_right ) {
599 $padding_right = '0px';
600 }
601 $layout_styles[] = array(
602 'selector' => "$selector > .alignfull",
603 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
604 );
605 }
606 if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
607 $padding_left = $block_spacing_values['declarations']['padding-left'];
608 // Add unit if 0.
609 if ( '0' === $padding_left ) {
610 $padding_left = '0px';
611 }
612 $layout_styles[] = array(
613 'selector' => "$selector > .alignfull",
614 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
615 );
616 }
617 }
618
619 if ( $has_justify_content_override && ! $should_output_constrained_sizes ) {
620 $layout_styles[] = array(
621 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
622 'declarations' => array(
623 'margin-left' => $margin_left,
624 'margin-right' => $margin_right,
625 ),
626 );
627 } elseif ( null === $viewport_overrides ) {
628 if ( 'left' === $justify_content ) {
629 $layout_styles[] = array(
630 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
631 'declarations' => array( 'margin-left' => '0 !important' ),
632 );
633 }
634
635 if ( 'right' === $justify_content ) {
636 $layout_styles[] = array(
637 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
638 'declarations' => array( 'margin-right' => '0 !important' ),
639 );
640 }
641 }
642
643 if ( $has_block_gap_support && $should_output_block_gap ) {
644 if ( is_array( $gap_value ) ) {
645 $gap_value = $gap_value['top'] ?? null;
646 }
647 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
648 // Get spacing CSS variable from preset value if provided.
649 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
650 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
651 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
652 $gap_value = "var(--wp--preset--spacing--$slug)";
653 }
654
655 array_push(
656 $layout_styles,
657 array(
658 'selector' => "$selector > *",
659 'declarations' => array(
660 'margin-block-start' => '0',
661 'margin-block-end' => '0',
662 ),
663 ),
664 array(
665 'selector' => "$selector > * + *",
666 'declarations' => array(
667 'margin-block-start' => $gap_value,
668 'margin-block-end' => '0',
669 ),
670 )
671 );
672 }
673 }
674 } elseif ( 'flex' === $layout_type ) {
675 $layout_orientation = $layout_for_styles['orientation'] ?? 'horizontal';
676
677 $justify_content_options = array(
678 'left' => 'flex-start',
679 'right' => 'flex-end',
680 'center' => 'center',
681 );
682
683 $vertical_alignment_options = array(
684 'top' => 'flex-start',
685 'center' => 'center',
686 'bottom' => 'flex-end',
687 );
688
689 if ( 'horizontal' === $layout_orientation ) {
690 $justify_content_options += array( 'space-between' => 'space-between' );
691 $vertical_alignment_options += array( 'stretch' => 'stretch' );
692 } else {
693 $justify_content_options += array( 'stretch' => 'stretch' );
694 $vertical_alignment_options += array( 'space-between' => 'space-between' );
695 }
696
697 /* 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
698 * value for any of the flex properties.
699 */
700 $should_output_flex_wrap = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
701 $should_output_flex_orientation = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
702 $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
703 $should_output_flex_alignment = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
704
705 if ( $should_output_flex_wrap && ! empty( $layout_for_styles['flexWrap'] ) && 'nowrap' === $layout_for_styles['flexWrap'] ) {
706 $layout_styles[] = array(
707 'selector' => $selector,
708 'declarations' => array( 'flex-wrap' => 'nowrap' ),
709 );
710 }
711
712 if ( $has_block_gap_support && $should_output_block_gap && isset( $gap_value ) ) {
713 $combined_gap_value = '';
714 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
715
716 foreach ( $gap_sides as $gap_side ) {
717 $process_value = $gap_value;
718 if ( is_array( $gap_value ) ) {
719 if ( is_array( $fallback_gap_value ) ) {
720 $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
721 } else {
722 $fallback_value = $fallback_gap_value;
723 }
724 $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
725 }
726 // Get spacing CSS variable from preset value if provided.
727 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
728 $index_to_splice = strrpos( $process_value, '|' ) + 1;
729 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
730 $process_value = "var(--wp--preset--spacing--$slug)";
731 }
732 $combined_gap_value .= "$process_value ";
733 }
734 $gap_value = trim( $combined_gap_value );
735
736 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
737 $layout_styles[] = array(
738 'selector' => $selector,
739 'declarations' => array( 'gap' => $gap_value ),
740 );
741 }
742 }
743
744 if ( 'horizontal' === $layout_orientation ) {
745 /*
746 * Add this style only if is not empty for backwards compatibility,
747 * since we intend to convert blocks that had flex layout implemented
748 * by custom css.
749 */
750 if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
751 $layout_styles[] = array(
752 'selector' => $selector,
753 'declarations' => array( 'justify-content' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
754 );
755 }
756
757 if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
758 $layout_styles[] = array(
759 'selector' => $selector,
760 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
761 );
762 }
763 } else {
764 if ( $should_output_flex_orientation ) {
765 $layout_styles[] = array(
766 'selector' => $selector,
767 'declarations' => array( 'flex-direction' => 'column' ),
768 );
769 }
770 if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
771 $layout_styles[] = array(
772 'selector' => $selector,
773 'declarations' => array( 'align-items' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
774 );
775 } elseif ( $should_output_flex_justification ) {
776 $layout_styles[] = array(
777 'selector' => $selector,
778 'declarations' => array( 'align-items' => 'flex-start' ),
779 );
780 }
781 if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
782 $layout_styles[] = array(
783 'selector' => $selector,
784 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
785 );
786 }
787 }
788 } elseif ( 'grid' === $layout_type ) {
789 /*
790 * If the gap value is an array, we use the "left" value because it represents the vertical gap, which
791 * is the relevant one for computation of responsive grid columns.
792 */
793 if ( is_array( $fallback_gap_value ) ) {
794 $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
795 } else {
796 $responsive_gap_value = $fallback_gap_value;
797 }
798
799 if ( $has_block_gap_support && isset( $gap_value ) ) {
800 $combined_gap_value = '';
801 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
802
803 foreach ( $gap_sides as $gap_side ) {
804 $process_value = $gap_value;
805 if ( is_array( $gap_value ) ) {
806 if ( is_array( $fallback_gap_value ) ) {
807 $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
808 } else {
809 $fallback_value = $fallback_gap_value;
810 }
811 $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
812 }
813 // Get spacing CSS variable from preset value if provided.
814 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
815 $index_to_splice = strrpos( $process_value, '|' ) + 1;
816 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
817 $process_value = "var(--wp--preset--spacing--$slug)";
818 }
819 $combined_gap_value .= "$process_value ";
820 }
821 $gap_value = trim( $combined_gap_value );
822 $responsive_gap_value = $gap_value;
823 }
824
825 // Ensure 0 values have a unit so they work in calc().
826 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
827 $responsive_gap_value = '0px';
828 }
829
830 /* 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
831 * value for any of the grid properties.
832 */
833 $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' ) || $has_viewport_property_override( 'autoFit' );
834 $uses_gap_in_grid_columns = ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
835 if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
836 $should_output_grid_columns = true;
837 }
838
839 $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['rowCount'] );
840 $grid_declarations = array();
841
842 /* When enabled, columns stretch to fill the available space using
843 * `auto-fit`; otherwise empty tracks are preserved with `auto-fill`.
844 */
845 $auto_placement = ! empty( $layout_for_styles['autoFit'] ) ? 'auto-fit' : 'auto-fill';
846
847 if ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
848 $max_value = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout_for_styles['columnCount'] . ' - 1))) /' . $layout_for_styles['columnCount'] . ')';
849 $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(' . $max_value . ', 1fr))';
850 } elseif ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) ) {
851 $grid_declarations['grid-template-columns'] = 'repeat(' . $layout_for_styles['columnCount'] . ', minmax(0, 1fr))';
852 } elseif ( $should_output_grid_columns ) {
853 $minimum_column_width = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
854 $grid_declarations['grid-template-columns'] = 'repeat(' . $auto_placement . ', minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
855 }
856
857 if ( ! empty( $grid_declarations ) ) {
858 $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
859 if ( empty( $layout_for_styles['columnCount'] ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
860 if ( null === $viewport_overrides || ! $base_has_container_type ) {
861 $grid_declarations['container-type'] = 'inline-size';
862 }
863 }
864 $layout_styles[] = array(
865 'selector' => $selector,
866 'declarations' => $grid_declarations,
867 );
868 }
869
870 if ( $should_output_grid_rows ) {
871 $layout_styles[] = array(
872 'selector' => $selector,
873 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout_for_styles['rowCount'] . ', minmax(1rem, auto))' ),
874 );
875 }
876
877 if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
878 $layout_styles[] = array(
879 'selector' => $selector,
880 'declarations' => array( 'gap' => $gap_value ),
881 );
882 }
883 }
884
885 if ( ! empty( $layout_styles ) ) {
886 if ( ! empty( $rules_group ) ) {
887 foreach ( $layout_styles as $index => $layout_style ) {
888 $layout_styles[ $index ]['rules_group'] = $rules_group;
889 }
890 }
891
892 /*
893 * Add to the style engine store to enqueue and render layout styles.
894 * Return compiled layout styles to retain backwards compatibility.
895 * Since https://github.com/WordPress/gutenberg/pull/42452,
896 * wp_enqueue_block_support_styles is no longer called in this block supports file.
897 */
898 return gutenberg_style_engine_get_stylesheet_from_css_rules(
899 $layout_styles,
900 array(
901 'context' => 'block-supports',
902 'prettify' => false,
903 )
904 );
905 }
906
907 return '';
908 }
909
910 /**
911 * Generates an incremental ID that is independent per each different prefix.
912 *
913 * It is similar to `wp_unique_id`, but each prefix has it's own internal ID
914 * counter to make each prefix independent from each other. The ID starts at 1
915 * and increments on each call. The returned value is not universally unique,
916 * but it is unique across the life of the PHP process and it's stable per
917 * prefix.
918 *
919 * @param string $prefix Prefix for the returned ID.
920 * @return string Incremental ID per prefix.
921 */
922 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
923 static $id_counters = array();
924 if ( ! array_key_exists( $prefix, $id_counters ) ) {
925 $id_counters[ $prefix ] = 0;
926 }
927 return $prefix . (string) ++$id_counters[ $prefix ];
928 }
929
930 /**
931 * Generates a unique ID based on the structure and values of a given array.
932 *
933 * This function serializes the array into a JSON string and generates a hash
934 * that serves as a unique identifier. Optionally, a prefix can be added to
935 * the generated ID for context or categorization.
936 *
937 * @param array $data The input array to generate an ID from.
938 * @param string $prefix Optional. A prefix to prepend to the generated ID. Default ''.
939 *
940 * @return string The generated unique ID for the array.
941 */
942 function gutenberg_unique_id_from_values( array $data, string $prefix = '' ): string {
943 $serialized = wp_json_encode( $data );
944 $hash = substr( md5( $serialized ), 0, 8 );
945 return $prefix . $hash;
946 }
947
948 /**
949 * Renders the layout config to the block wrapper.
950 *
951 * @param string $block_content Rendered block content.
952 * @param array $block Block object.
953 * @return string Filtered block content.
954 */
955 function gutenberg_render_layout_support_flag( $block_content, $block ) {
956 static $global_styles = null;
957
958 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
959 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
960 $style_attr = gutenberg_resolve_style_state_aliases(
961 $block['attrs']['style'] ?? array(),
962 $block['blockName']
963 );
964 $global_settings = gutenberg_get_global_settings();
965 $viewport_settings = $global_settings['viewport'] ?? null;
966 $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );
967 // If there is any value in style -> layout, the block has a child layout.
968 $child_layout = $style_attr['layout'] ?? null;
969
970 // Collect responsive viewport child layout overrides so that a block with
971 // only responsive child layout (no base child layout) is still processed.
972 $viewport_child_layouts = array();
973 foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
974 $viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
975 if ( ! empty( $viewport_child ) ) {
976 $viewport_child_layouts[ $breakpoint ] = array(
977 'media_query' => $media_query,
978 'child_layout' => $viewport_child,
979 );
980 }
981 }
982
983 if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
984 return $block_content;
985 }
986
987 $outer_class_names = array();
988
989 // Child layout specific logic.
990 if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
991 $base_child_layout = gutenberg_get_layout_child_values( $child_layout );
992 $parent_layout = $block['parentLayout'] ?? array();
993
994 /*
995 * Generates a unique class for child block layout styles.
996 *
997 * To ensure consistent class generation across different page renders,
998 * only properties that affect layout styling are used. These properties
999 * come from `$block['attrs']['style']['layout']`, viewport overrides in
1000 * `$block['attrs']['style'][$breakpoint]['layout']`, and
1001 * `$block['parentLayout']`.
1002 *
1003 * As long as these properties coincide, the generated class will be the same.
1004 */
1005 $container_content_hash_input = array(
1006 'layout' => $base_child_layout,
1007 'parentLayout' => array_intersect_key(
1008 $parent_layout,
1009 array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
1010 ),
1011 );
1012 foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
1013 $container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
1014 }
1015 $container_content_class = gutenberg_unique_id_from_values(
1016 $container_content_hash_input,
1017 'wp-container-content-'
1018 );
1019
1020 $child_layout_styles = gutenberg_get_child_layout_style_rules(
1021 ".$container_content_class",
1022 $base_child_layout,
1023 $parent_layout
1024 );
1025
1026 // Emit responsive child layout CSS using the same container-content class
1027 // so that base and responsive child layout share the exact same selector.
1028 foreach ( $viewport_child_layouts as $viewport_data ) {
1029 $viewport_child_styles = gutenberg_get_child_layout_style_rules(
1030 ".$container_content_class",
1031 $base_child_layout,
1032 $parent_layout,
1033 $viewport_data['child_layout']
1034 );
1035 foreach ( $viewport_child_styles as $index => $rule ) {
1036 $viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
1037 }
1038
1039 $child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
1040 }
1041
1042 /*
1043 * Add to the style engine store to enqueue and render layout styles.
1044 * Return styles here just to check if any exist.
1045 */
1046 $child_css = gutenberg_style_engine_get_stylesheet_from_css_rules(
1047 $child_layout_styles,
1048 array(
1049 'context' => 'block-supports',
1050 'prettify' => false,
1051 )
1052 );
1053
1054 if ( $child_css ) {
1055 $outer_class_names[] = $container_content_class;
1056 }
1057 }
1058
1059 // Prep the processor for modifying the block output.
1060 $processor = new WP_HTML_Tag_Processor( $block_content );
1061
1062 // Having no tags implies there are no tags onto which to add class names.
1063 if ( ! $processor->next_tag() ) {
1064 return $block_content;
1065 }
1066
1067 /*
1068 * A block may not support layout but still be affected by a parent block's layout.
1069 *
1070 * In these cases add the appropriate class names and then return early; there's
1071 * no need to investigate on this block whether additional layout constraints apply.
1072 */
1073 if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
1074 foreach ( $outer_class_names as $class_name ) {
1075 $processor->add_class( $class_name );
1076 }
1077 return $processor->get_updated_html();
1078 } elseif ( ! $block_supports_layout ) {
1079 // Ensure layout classnames are not injected if there is no layout support.
1080 return $block_content;
1081 }
1082
1083 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
1084 if ( empty( $fallback_layout ) ) {
1085 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
1086 }
1087 $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
1088
1089 $class_names = array();
1090 $layout_definitions = gutenberg_get_layout_definitions();
1091
1092 // Set the correct layout type for blocks using legacy content width.
1093 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
1094 $used_layout['type'] = 'constrained';
1095 }
1096
1097 $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
1098
1099 if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) {
1100 $class_names[] = 'has-global-padding';
1101 }
1102
1103 /*
1104 * The following section was added to reintroduce a small set of layout classnames that were
1105 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
1106 * not intended to provide an extended set of classes to match all block layout attributes
1107 * here.
1108 */
1109 if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
1110 $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
1111 }
1112
1113 if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
1114 $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
1115 }
1116
1117 if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
1118 $class_names[] = 'is-nowrap';
1119 }
1120
1121 // Get classname for layout type.
1122 if ( isset( $used_layout['type'] ) ) {
1123 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
1124 } else {
1125 $layout_classname = $layout_definitions['default']['className'] ?? '';
1126 }
1127
1128 if ( $layout_classname && is_string( $layout_classname ) ) {
1129 $class_names[] = sanitize_title( $layout_classname );
1130 }
1131
1132 /*
1133 * Only generate Layout styles if the theme has not opted-out.
1134 * Attribute-based Layout classnames are output in all cases.
1135 */
1136 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
1137
1138 $gap_value = gutenberg_sanitize_block_gap_value( $block['attrs']['style']['spacing']['blockGap'] ?? null );
1139
1140 $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
1141 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
1142
1143 /*
1144 * If a block's block.json skips serialization for spacing or spacing.blockGap,
1145 * don't apply the user-defined value to the styles.
1146 */
1147 $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
1148
1149 $block_gap = $global_settings['spacing']['blockGap'] ?? null;
1150 $has_block_gap_support = isset( $block_gap );
1151
1152 // Get default blockGap value from global styles for use in layouts like grid.
1153 // Check style variation first, then block-specific styles, then fall back to root styles.
1154 $block_name = $block['blockName'] ?? '';
1155 if ( null === $global_styles ) {
1156 $global_styles = gutenberg_get_global_styles();
1157 }
1158
1159 // Check if the block has an active style variation with a blockGap value.
1160 // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
1161 $variation_block_gap_value = null;
1162 $block_class_name = is_string( $block['attrs']['className'] ?? null )
1163 ? $block['attrs']['className']
1164 : '';
1165 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1166 $styles_registry = WP_Block_Styles_Registry::get_instance();
1167 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1168 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
1169 if ( $variation_name ) {
1170 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1171 }
1172 }
1173
1174 $global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;
1175
1176 if ( null !== $global_block_gap_value ) {
1177 $fallback_gap_value = $global_block_gap_value;
1178 }
1179
1180 /*
1181 * We generate a unique ID based on all the data required to obtain the
1182 * corresponding layout style. This way, the CSS class names keep the same
1183 * even for different blocks with the same layout definition. We need this to
1184 * make the CSS class names stable across paginations for features like the
1185 * enhanced pagination of the Query block.
1186 */
1187 $container_class_hash_input = array(
1188 $used_layout,
1189 $has_block_gap_support,
1190 $gap_value,
1191 $should_skip_gap_serialization,
1192 $fallback_gap_value,
1193 $block_spacing,
1194 );
1195
1196 foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
1197 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1198 if ( ! is_array( $viewport_style ) ) {
1199 continue;
1200 }
1201
1202 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1203 if ( ! empty( $viewport_container_layout ) ) {
1204 $container_class_hash_input[] = array(
1205 'breakpoint' => $breakpoint,
1206 'layout' => $viewport_container_layout,
1207 );
1208 }
1209
1210 if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
1211 $container_class_hash_input[] = array(
1212 'breakpoint' => $breakpoint,
1213 'blockGap' => gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
1214 );
1215 }
1216 }
1217
1218 $container_class = gutenberg_unique_id_from_values(
1219 $container_class_hash_input,
1220 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
1221 );
1222
1223 $style = gutenberg_get_layout_style(
1224 ".$container_class",
1225 $used_layout,
1226 $has_block_gap_support,
1227 $gap_value,
1228 $should_skip_gap_serialization,
1229 $fallback_gap_value,
1230 $block_spacing
1231 );
1232
1233 // Only add container class and enqueue block support styles if unique styles were generated.
1234 if ( ! empty( $style ) ) {
1235 $class_names[] = $container_class;
1236 }
1237
1238 /*
1239 * Emit responsive container layout styles using the same $container_class
1240 * selector as the base layout so they target the inner block wrapper.
1241 */
1242 foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1243 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1244 if ( ! is_array( $viewport_style ) ) {
1245 continue;
1246 }
1247
1248 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1249 $has_viewport_layout = ! empty( $viewport_container_layout );
1250 $has_viewport_block_gap = isset( $viewport_style['spacing']['blockGap'] );
1251
1252 if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
1253 continue;
1254 }
1255
1256 $viewport_gap_value = $has_viewport_block_gap
1257 ? gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
1258 : $gap_value;
1259 $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
1260 ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
1261 : $block_spacing;
1262
1263 $viewport_styles = gutenberg_get_layout_style(
1264 ".$container_class",
1265 $used_layout,
1266 $has_block_gap_support,
1267 $viewport_gap_value,
1268 $should_skip_gap_serialization,
1269 $fallback_gap_value,
1270 $viewport_block_spacing,
1271 array(
1272 'rules_group' => $media_query,
1273 'viewport_overrides' => $viewport_container_layout,
1274 'has_block_gap_override' => $has_viewport_block_gap,
1275 )
1276 );
1277
1278 if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
1279 $class_names[] = $container_class;
1280 }
1281 }
1282 }
1283
1284 // Add combined layout and block classname for global styles to hook onto.
1285 $split_block_name = explode( '/', $block['blockName'] );
1286 $full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
1287 $class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
1288
1289 // Add classes to the outermost HTML tag if necessary.
1290 if ( ! empty( $outer_class_names ) ) {
1291 foreach ( $outer_class_names as $outer_class_name ) {
1292 $processor->add_class( $outer_class_name );
1293 }
1294 }
1295
1296 /*
1297 * Attempts to refer to the inner-block wrapping element by its class attribute.
1298 *
1299 * When examining a block's inner content, if a block has inner blocks, then
1300 * the first content item will likely be a text (HTML) chunk immediately
1301 * preceding the inner blocks. The last HTML tag in that chunk would then be
1302 * an opening tag for an element that wraps the inner blocks.
1303 *
1304 * There's no reliable way to associate this wrapper in $block_content because
1305 * it may have changed during the rendering pipeline (as inner contents is
1306 * provided before rendering) and through previous filters. In many cases,
1307 * however, the `class` attribute will be a good-enough identifier, so this
1308 * code finds the last tag in that chunk and stores the `class` attribute
1309 * so that it can be used later when working through the rendered block output
1310 * to identify the wrapping element and add the remaining class names to it.
1311 *
1312 * It's also possible that no inner block wrapper even exists. If that's the
1313 * case this code could apply the class names to an invalid element.
1314 *
1315 * Example:
1316 *
1317 * $block['innerBlocks'] = array( $list_item );
1318 * $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
1319 *
1320 * // After rendering, the initial contents may have been modified by other renderers or filters.
1321 * $block_content = <<<HTML
1322 * <figure>
1323 * <ul class="annotated-list list-wrapper is-unordered">
1324 * <li>Code</li>
1325 * </ul><figcaption>It's a list!</figcaption>
1326 * </figure>
1327 * HTML;
1328 *
1329 * Although it is possible that the original block-wrapper classes are changed in $block_content
1330 * from how they appear in $block['innerContent'], it's likely that the original class attributes
1331 * are still present in the wrapper as they are in this example. Frequently, additional classes
1332 * will also be present; rarely should classes be removed.
1333 *
1334 * @todo Find a better way to match the first inner block. If it's possible to identify where the
1335 * first inner block starts, then it will be possible to find the last tag before it starts
1336 * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
1337 * Can some unique value or class or ID be added to the inner blocks when they process
1338 * so that they can be extracted here safely without guessing? Can the block rendering function
1339 * return information about where the rendered inner blocks start?
1340 *
1341 * @var string|null
1342 */
1343 $inner_block_wrapper_classes = null;
1344 $first_chunk = $block['innerContent'][0] ?? null;
1345 if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
1346 $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
1347 /*
1348 * Use a stack to track open elements as tags are visited. Void elements
1349 * (those without a matching closing tag) are excluded so they don't
1350 * accumulate on the stack. At the end of the chunk, every element still
1351 * on the stack is unclosed — meaning its closing tag lives in a later
1352 * innerContent entry alongside the inner blocks, which makes it the
1353 * inner-block container. Elements that open and close within this chunk
1354 * are siblings that precede the inner blocks and should be ignored.
1355 * The last unclosed element with a class attribute is the best candidate
1356 * for the inner-block wrapper.
1357 */
1358 $tag_stack = array();
1359 while ( $first_chunk_processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
1360 if ( $first_chunk_processor->is_tag_closer() ) {
1361 array_pop( $tag_stack );
1362 } elseif ( ! WP_HTML_Processor::is_void( $first_chunk_processor->get_tag() ) ) {
1363 $tag_stack[] = $first_chunk_processor->get_attribute( 'class' );
1364 }
1365 }
1366 foreach ( array_reverse( $tag_stack ) as $class_attribute ) {
1367 if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
1368 $inner_block_wrapper_classes = $class_attribute;
1369 break;
1370 }
1371 }
1372 }
1373
1374 /*
1375 * If necessary, advance to what is likely to be an inner block wrapper tag.
1376 *
1377 * This advances until it finds the first tag containing the original class
1378 * attribute from above. If none is found it will scan to the end of the block
1379 * and fail to add any class names.
1380 *
1381 * If there is no block wrapper it won't advance at all, in which case the
1382 * class names will be added to the first and outermost tag of the block.
1383 * For cases where this outermost tag is the only tag surrounding inner
1384 * blocks then the outer wrapper and inner wrapper are the same.
1385 */
1386 do {
1387 if ( ! $inner_block_wrapper_classes ) {
1388 break;
1389 }
1390
1391 $class_attribute = $processor->get_attribute( 'class' );
1392 if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
1393 break;
1394 }
1395 } while ( $processor->next_tag() );
1396
1397 // Add the remaining class names.
1398 foreach ( $class_names as $class_name ) {
1399 $processor->add_class( $class_name );
1400 }
1401
1402 return $processor->get_updated_html();
1403 }
1404
1405 /*
1406 * Add a `render_block_data` filter to fetch the parent block layout data.
1407 */
1408 add_filter(
1409 'render_block_data',
1410 function ( $parsed_block, $source_block, $parent_block ) {
1411 /*
1412 * Check if the parent block exists and if it has a layout attribute.
1413 * If it does, add the parent layout to the parsed block.
1414 */
1415 if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) {
1416 $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout'];
1417 }
1418 return $parsed_block;
1419 },
1420 10,
1421 3
1422 );
1423
1424 // Register the block support. (overrides core one).
1425 WP_Block_Supports::get_instance()->register(
1426 'layout',
1427 array(
1428 'register_attribute' => 'gutenberg_register_layout_support',
1429 )
1430 );
1431
1432 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
1433 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
1434 }
1435 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
1436
1437 /**
1438 * For themes without theme.json file, make sure
1439 * to restore the inner div for the group block
1440 * to avoid breaking styles relying on that div.
1441 *
1442 * @param string $block_content Rendered block content.
1443 * @param array $block Block object.
1444 * @return string Filtered block content.
1445 */
1446 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1447 $tag_name = $block['attrs']['tagName'] ?? 'div';
1448 $group_with_inner_container_regex = sprintf(
1449 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1450 preg_quote( $tag_name, '/' )
1451 );
1452 if (
1453 wp_theme_has_theme_json() ||
1454 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
1455 ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
1456 ) {
1457 return $block_content;
1458 }
1459
1460 /*
1461 * This filter runs after the layout classnames have been added to the block, so they
1462 * have to be removed from the outer wrapper and then added to the inner.
1463 */
1464 $layout_classes = array();
1465 $processor = new WP_HTML_Tag_Processor( $block_content );
1466
1467 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
1468 foreach ( $processor->class_list() as $class_name ) {
1469 if ( str_contains( $class_name, 'layout' ) ) {
1470 array_push( $layout_classes, $class_name );
1471 $processor->remove_class( $class_name );
1472 }
1473 }
1474 }
1475
1476 $content_without_layout_classes = $processor->get_updated_html();
1477 $replace_regex = sprintf(
1478 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
1479 preg_quote( $tag_name, '/' )
1480 );
1481 $updated_content = preg_replace_callback(
1482 $replace_regex,
1483 static function ( $matches ) {
1484 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
1485 },
1486 $content_without_layout_classes
1487 );
1488
1489 // Add layout classes to inner wrapper.
1490 if ( ! empty( $layout_classes ) ) {
1491 $processor = new WP_HTML_Tag_Processor( $updated_content );
1492 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) {
1493 foreach ( $layout_classes as $class_name ) {
1494 $processor->add_class( $class_name );
1495 }
1496 }
1497 $updated_content = $processor->get_updated_html();
1498 }
1499
1500 return $updated_content;
1501 }
1502
1503 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
1504 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10 );
1505 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10 );
1506 }
1507 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
1508
1509 /**
1510 * For themes without theme.json file, make sure
1511 * to restore the outer div for the aligned image block
1512 * to avoid breaking styles relying on that div.
1513 *
1514 * @param string $block_content Rendered block content.
1515 * @param array $block Block object.
1516 * @return string Filtered block content.
1517 */
1518 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1519 if ( wp_theme_has_theme_json() ) {
1520 return $block_content;
1521 }
1522
1523 $figure_processor = new WP_HTML_Tag_Processor( $block_content );
1524 if (
1525 ! $figure_processor->next_tag( 'FIGURE' ) ||
1526 ! $figure_processor->has_class( 'wp-block-image' ) ||
1527 ! (
1528 $figure_processor->has_class( 'alignleft' ) ||
1529 $figure_processor->has_class( 'aligncenter' ) ||
1530 $figure_processor->has_class( 'alignright' )
1531 )
1532 ) {
1533 return $block_content;
1534 }
1535
1536 /*
1537 * The next section of code wraps the existing figure in a new DIV element.
1538 * While doing it, it needs to transfer the layout and the additional CSS
1539 * class names from the original figure upward to the wrapper.
1540 *
1541 * Example:
1542 *
1543 * // From this…
1544 * <!-- wp:image {"className":"hires"} -->
1545 * <figure class="wp-block-image wide hires">…
1546 *
1547 * // To this…
1548 * <div class="wp-block-image hires"><figure class="wide">…
1549 */
1550 $wrapper_processor = new WP_HTML_Tag_Processor( '<div>' );
1551 $wrapper_processor->next_token();
1552 $wrapper_processor->set_attribute(
1553 'class',
1554 is_string( $block['attrs']['className'] ?? null )
1555 ? "wp-block-image {$block['attrs']['className']}"
1556 : 'wp-block-image'
1557 );
1558
1559 // And remove them from the existing content; it has been transferred upward.
1560 $figure_processor->remove_class( 'wp-block-image' );
1561 foreach ( $wrapper_processor->class_list() as $class_name ) {
1562 $figure_processor->remove_class( $class_name );
1563 }
1564
1565 return "{$wrapper_processor->get_updated_html()}{$figure_processor->get_updated_html()}</div>";
1566 }
1567
1568 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
1569 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10 );
1570 }
1571 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
1572