PluginProbe
Gutenberg / 23.5.2
Gutenberg v23.5.2
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.5.2, at lib/block-supports/layout.php

1,536 lines 57.5 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 ] = $value && preg_match( '%[\\\(&=}]|/\*%', $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 $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
483 return array_key_exists( $property, $viewport_overrides );
484 };
485 $layout_styles = array();
486
487 if ( 'default' === $layout_type ) {
488 if ( $has_block_gap_support && $should_output_block_gap ) {
489 if ( is_array( $gap_value ) ) {
490 $gap_value = $gap_value['top'] ?? null;
491 }
492 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
493 // Get spacing CSS variable from preset value if provided.
494 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
495 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
496 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
497 $gap_value = "var(--wp--preset--spacing--$slug)";
498 }
499
500 array_push(
501 $layout_styles,
502 array(
503 'selector' => "$selector > *",
504 'declarations' => array(
505 'margin-block-start' => '0',
506 'margin-block-end' => '0',
507 ),
508 ),
509 array(
510 'selector' => "$selector > * + *",
511 'declarations' => array(
512 'margin-block-start' => $gap_value,
513 'margin-block-end' => '0',
514 ),
515 )
516 );
517 }
518 }
519 } elseif ( 'constrained' === $layout_type ) {
520 $content_size = $layout_for_styles['contentSize'] ?? '';
521 $wide_size = $layout_for_styles['wideSize'] ?? '';
522 $justify_content = $layout_for_styles['justifyContent'] ?? 'center';
523
524 $all_max_width_value = $content_size ? $content_size : $wide_size;
525 $wide_max_width_value = $wide_size ? $wide_size : $content_size;
526
527 // Make sure there is a single CSS rule, and all tags are stripped for security.
528 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
529 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
530
531 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
532 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
533
534 $has_justify_content_override = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
535 $should_output_constrained_sizes = null === $viewport_overrides || $has_viewport_property_override( 'contentSize' ) || $has_viewport_property_override( 'wideSize' );
536 if ( $should_output_constrained_sizes && ( $content_size || $wide_size ) ) {
537 $content_size_declarations = array(
538 'max-width' => $all_max_width_value,
539 );
540
541 if ( null === $viewport_overrides || $has_justify_content_override ) {
542 $content_size_declarations['margin-left'] = $margin_left;
543 $content_size_declarations['margin-right'] = $margin_right;
544 }
545
546 array_push(
547 $layout_styles,
548 array(
549 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
550 'declarations' => $content_size_declarations,
551 ),
552 array(
553 'selector' => "$selector > .alignwide",
554 'declarations' => array( 'max-width' => $wide_max_width_value ),
555 ),
556 array(
557 'selector' => "$selector .alignfull",
558 'declarations' => array( 'max-width' => 'none' ),
559 )
560 );
561 }
562
563 if ( null === $viewport_overrides && isset( $block_spacing ) ) {
564 $block_spacing_values = gutenberg_style_engine_get_styles(
565 array(
566 'spacing' => $block_spacing,
567 )
568 );
569
570 /*
571 * Handle negative margins for alignfull children of blocks with custom padding set.
572 * They're added separately because padding might only be set on one side.
573 */
574 if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
575 $padding_right = $block_spacing_values['declarations']['padding-right'];
576 // Add unit if 0.
577 if ( '0' === $padding_right ) {
578 $padding_right = '0px';
579 }
580 $layout_styles[] = array(
581 'selector' => "$selector > .alignfull",
582 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
583 );
584 }
585 if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
586 $padding_left = $block_spacing_values['declarations']['padding-left'];
587 // Add unit if 0.
588 if ( '0' === $padding_left ) {
589 $padding_left = '0px';
590 }
591 $layout_styles[] = array(
592 'selector' => "$selector > .alignfull",
593 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
594 );
595 }
596 }
597
598 if ( $has_justify_content_override && ! $should_output_constrained_sizes ) {
599 $layout_styles[] = array(
600 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
601 'declarations' => array(
602 'margin-left' => $margin_left,
603 'margin-right' => $margin_right,
604 ),
605 );
606 } elseif ( null === $viewport_overrides ) {
607 if ( 'left' === $justify_content ) {
608 $layout_styles[] = array(
609 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
610 'declarations' => array( 'margin-left' => '0 !important' ),
611 );
612 }
613
614 if ( 'right' === $justify_content ) {
615 $layout_styles[] = array(
616 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
617 'declarations' => array( 'margin-right' => '0 !important' ),
618 );
619 }
620 }
621
622 if ( $has_block_gap_support && $should_output_block_gap ) {
623 if ( is_array( $gap_value ) ) {
624 $gap_value = $gap_value['top'] ?? null;
625 }
626 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
627 // Get spacing CSS variable from preset value if provided.
628 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
629 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
630 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
631 $gap_value = "var(--wp--preset--spacing--$slug)";
632 }
633
634 array_push(
635 $layout_styles,
636 array(
637 'selector' => "$selector > *",
638 'declarations' => array(
639 'margin-block-start' => '0',
640 'margin-block-end' => '0',
641 ),
642 ),
643 array(
644 'selector' => "$selector > * + *",
645 'declarations' => array(
646 'margin-block-start' => $gap_value,
647 'margin-block-end' => '0',
648 ),
649 )
650 );
651 }
652 }
653 } elseif ( 'flex' === $layout_type ) {
654 $layout_orientation = $layout_for_styles['orientation'] ?? 'horizontal';
655
656 $justify_content_options = array(
657 'left' => 'flex-start',
658 'right' => 'flex-end',
659 'center' => 'center',
660 );
661
662 $vertical_alignment_options = array(
663 'top' => 'flex-start',
664 'center' => 'center',
665 'bottom' => 'flex-end',
666 );
667
668 if ( 'horizontal' === $layout_orientation ) {
669 $justify_content_options += array( 'space-between' => 'space-between' );
670 $vertical_alignment_options += array( 'stretch' => 'stretch' );
671 } else {
672 $justify_content_options += array( 'stretch' => 'stretch' );
673 $vertical_alignment_options += array( 'space-between' => 'space-between' );
674 }
675
676 $should_output_flex_wrap = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
677 $should_output_flex_orientation = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
678 $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
679 $should_output_flex_alignment = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
680
681 if ( $should_output_flex_wrap && ! empty( $layout_for_styles['flexWrap'] ) && 'nowrap' === $layout_for_styles['flexWrap'] ) {
682 $layout_styles[] = array(
683 'selector' => $selector,
684 'declarations' => array( 'flex-wrap' => 'nowrap' ),
685 );
686 }
687
688 if ( $has_block_gap_support && $should_output_block_gap && isset( $gap_value ) ) {
689 $combined_gap_value = '';
690 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
691
692 foreach ( $gap_sides as $gap_side ) {
693 $process_value = $gap_value;
694 if ( is_array( $gap_value ) ) {
695 if ( is_array( $fallback_gap_value ) ) {
696 $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
697 } else {
698 $fallback_value = $fallback_gap_value;
699 }
700 $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
701 }
702 // Get spacing CSS variable from preset value if provided.
703 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
704 $index_to_splice = strrpos( $process_value, '|' ) + 1;
705 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
706 $process_value = "var(--wp--preset--spacing--$slug)";
707 }
708 $combined_gap_value .= "$process_value ";
709 }
710 $gap_value = trim( $combined_gap_value );
711
712 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
713 $layout_styles[] = array(
714 'selector' => $selector,
715 'declarations' => array( 'gap' => $gap_value ),
716 );
717 }
718 }
719
720 if ( 'horizontal' === $layout_orientation ) {
721 /*
722 * Add this style only if is not empty for backwards compatibility,
723 * since we intend to convert blocks that had flex layout implemented
724 * by custom css.
725 */
726 if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
727 $layout_styles[] = array(
728 'selector' => $selector,
729 'declarations' => array( 'justify-content' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
730 );
731 }
732
733 if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
734 $layout_styles[] = array(
735 'selector' => $selector,
736 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
737 );
738 }
739 } else {
740 if ( $should_output_flex_orientation ) {
741 $layout_styles[] = array(
742 'selector' => $selector,
743 'declarations' => array( 'flex-direction' => 'column' ),
744 );
745 }
746 if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
747 $layout_styles[] = array(
748 'selector' => $selector,
749 'declarations' => array( 'align-items' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
750 );
751 } elseif ( $should_output_flex_justification ) {
752 $layout_styles[] = array(
753 'selector' => $selector,
754 'declarations' => array( 'align-items' => 'flex-start' ),
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( 'justify-content' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
761 );
762 }
763 }
764 } elseif ( 'grid' === $layout_type ) {
765 /*
766 * If the gap value is an array, we use the "left" value because it represents the vertical gap, which
767 * is the relevant one for computation of responsive grid columns.
768 */
769 if ( is_array( $fallback_gap_value ) ) {
770 $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value );
771 } else {
772 $responsive_gap_value = $fallback_gap_value;
773 }
774
775 if ( $has_block_gap_support && isset( $gap_value ) ) {
776 $combined_gap_value = '';
777 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
778
779 foreach ( $gap_sides as $gap_side ) {
780 $process_value = $gap_value;
781 if ( is_array( $gap_value ) ) {
782 if ( is_array( $fallback_gap_value ) ) {
783 $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value );
784 } else {
785 $fallback_value = $fallback_gap_value;
786 }
787 $process_value = $gap_value[ $gap_side ] ?? $fallback_value;
788 }
789 // Get spacing CSS variable from preset value if provided.
790 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
791 $index_to_splice = strrpos( $process_value, '|' ) + 1;
792 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
793 $process_value = "var(--wp--preset--spacing--$slug)";
794 }
795 $combined_gap_value .= "$process_value ";
796 }
797 $gap_value = trim( $combined_gap_value );
798 $responsive_gap_value = $gap_value;
799 }
800
801 // Ensure 0 values have a unit so they work in calc().
802 if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) {
803 $responsive_gap_value = '0px';
804 }
805
806 $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' );
807 $uses_gap_in_grid_columns = ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
808 if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
809 $should_output_grid_columns = true;
810 }
811
812 $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['rowCount'] );
813 $grid_declarations = array();
814
815 if ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
816 $max_value = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout_for_styles['columnCount'] . ' - 1))) /' . $layout_for_styles['columnCount'] . ')';
817 $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))';
818 } elseif ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) ) {
819 $grid_declarations['grid-template-columns'] = 'repeat(' . $layout_for_styles['columnCount'] . ', minmax(0, 1fr))';
820 } elseif ( $should_output_grid_columns ) {
821 $minimum_column_width = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
822 $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
823 }
824
825 if ( ! empty( $grid_declarations ) ) {
826 $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
827 if ( empty( $layout_for_styles['columnCount'] ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
828 if ( null === $viewport_overrides || ! $base_has_container_type ) {
829 $grid_declarations['container-type'] = 'inline-size';
830 }
831 }
832 $layout_styles[] = array(
833 'selector' => $selector,
834 'declarations' => $grid_declarations,
835 );
836 }
837
838 if ( $should_output_grid_rows ) {
839 $layout_styles[] = array(
840 'selector' => $selector,
841 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout_for_styles['rowCount'] . ', minmax(1rem, auto))' ),
842 );
843 }
844
845 if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
846 $layout_styles[] = array(
847 'selector' => $selector,
848 'declarations' => array( 'gap' => $gap_value ),
849 );
850 }
851 }
852
853 if ( ! empty( $layout_styles ) ) {
854 if ( ! empty( $rules_group ) ) {
855 foreach ( $layout_styles as $index => $layout_style ) {
856 $layout_styles[ $index ]['rules_group'] = $rules_group;
857 }
858 }
859
860 /*
861 * Add to the style engine store to enqueue and render layout styles.
862 * Return compiled layout styles to retain backwards compatibility.
863 * Since https://github.com/WordPress/gutenberg/pull/42452,
864 * wp_enqueue_block_support_styles is no longer called in this block supports file.
865 */
866 return gutenberg_style_engine_get_stylesheet_from_css_rules(
867 $layout_styles,
868 array(
869 'context' => 'block-supports',
870 'prettify' => false,
871 )
872 );
873 }
874
875 return '';
876 }
877
878 /**
879 * Generates an incremental ID that is independent per each different prefix.
880 *
881 * It is similar to `wp_unique_id`, but each prefix has it's own internal ID
882 * counter to make each prefix independent from each other. The ID starts at 1
883 * and increments on each call. The returned value is not universally unique,
884 * but it is unique across the life of the PHP process and it's stable per
885 * prefix.
886 *
887 * @param string $prefix Prefix for the returned ID.
888 * @return string Incremental ID per prefix.
889 */
890 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
891 static $id_counters = array();
892 if ( ! array_key_exists( $prefix, $id_counters ) ) {
893 $id_counters[ $prefix ] = 0;
894 }
895 return $prefix . (string) ++$id_counters[ $prefix ];
896 }
897
898 /**
899 * Generates a unique ID based on the structure and values of a given array.
900 *
901 * This function serializes the array into a JSON string and generates a hash
902 * that serves as a unique identifier. Optionally, a prefix can be added to
903 * the generated ID for context or categorization.
904 *
905 * @param array $data The input array to generate an ID from.
906 * @param string $prefix Optional. A prefix to prepend to the generated ID. Default ''.
907 *
908 * @return string The generated unique ID for the array.
909 */
910 function gutenberg_unique_id_from_values( array $data, string $prefix = '' ): string {
911 $serialized = wp_json_encode( $data );
912 $hash = substr( md5( $serialized ), 0, 8 );
913 return $prefix . $hash;
914 }
915
916 /**
917 * Renders the layout config to the block wrapper.
918 *
919 * @param string $block_content Rendered block content.
920 * @param array $block Block object.
921 * @return string Filtered block content.
922 */
923 function gutenberg_render_layout_support_flag( $block_content, $block ) {
924 static $global_styles = null;
925
926 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
927 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
928 $style_attr = gutenberg_resolve_style_state_aliases(
929 $block['attrs']['style'] ?? array(),
930 $block['blockName']
931 );
932 // If there is any value in style -> layout, the block has a child layout.
933 $child_layout = $style_attr['layout'] ?? null;
934
935 // Collect responsive viewport child layout overrides so that a block with
936 // only responsive child layout (no base child layout) is still processed.
937 $viewport_child_layouts = array();
938 foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
939 $viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
940 if ( ! empty( $viewport_child ) ) {
941 $viewport_child_layouts[ $breakpoint ] = array(
942 'media_query' => $media_query,
943 'child_layout' => $viewport_child,
944 );
945 }
946 }
947
948 if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
949 return $block_content;
950 }
951
952 $outer_class_names = array();
953
954 // Child layout specific logic.
955 if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
956 $base_child_layout = gutenberg_get_layout_child_values( $child_layout );
957 $parent_layout = $block['parentLayout'] ?? array();
958
959 /*
960 * Generates a unique class for child block layout styles.
961 *
962 * To ensure consistent class generation across different page renders,
963 * only properties that affect layout styling are used. These properties
964 * come from `$block['attrs']['style']['layout']`, viewport overrides in
965 * `$block['attrs']['style'][$breakpoint]['layout']`, and
966 * `$block['parentLayout']`.
967 *
968 * As long as these properties coincide, the generated class will be the same.
969 */
970 $container_content_hash_input = array(
971 'layout' => $base_child_layout,
972 'parentLayout' => array_intersect_key(
973 $parent_layout,
974 array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
975 ),
976 );
977 foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
978 $container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
979 }
980 $container_content_class = gutenberg_unique_id_from_values(
981 $container_content_hash_input,
982 'wp-container-content-'
983 );
984
985 $child_layout_styles = gutenberg_get_child_layout_style_rules(
986 ".$container_content_class",
987 $base_child_layout,
988 $parent_layout
989 );
990
991 // Emit responsive child layout CSS using the same container-content class
992 // so that base and responsive child layout share the exact same selector.
993 foreach ( $viewport_child_layouts as $viewport_data ) {
994 $viewport_child_styles = gutenberg_get_child_layout_style_rules(
995 ".$container_content_class",
996 $base_child_layout,
997 $parent_layout,
998 $viewport_data['child_layout']
999 );
1000 foreach ( $viewport_child_styles as $index => $rule ) {
1001 $viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
1002 }
1003
1004 $child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
1005 }
1006
1007 /*
1008 * Add to the style engine store to enqueue and render layout styles.
1009 * Return styles here just to check if any exist.
1010 */
1011 $child_css = gutenberg_style_engine_get_stylesheet_from_css_rules(
1012 $child_layout_styles,
1013 array(
1014 'context' => 'block-supports',
1015 'prettify' => false,
1016 )
1017 );
1018
1019 if ( $child_css ) {
1020 $outer_class_names[] = $container_content_class;
1021 }
1022 }
1023
1024 // Prep the processor for modifying the block output.
1025 $processor = new WP_HTML_Tag_Processor( $block_content );
1026
1027 // Having no tags implies there are no tags onto which to add class names.
1028 if ( ! $processor->next_tag() ) {
1029 return $block_content;
1030 }
1031
1032 /*
1033 * A block may not support layout but still be affected by a parent block's layout.
1034 *
1035 * In these cases add the appropriate class names and then return early; there's
1036 * no need to investigate on this block whether additional layout constraints apply.
1037 */
1038 if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
1039 foreach ( $outer_class_names as $class_name ) {
1040 $processor->add_class( $class_name );
1041 }
1042 return $processor->get_updated_html();
1043 } elseif ( ! $block_supports_layout ) {
1044 // Ensure layout classnames are not injected if there is no layout support.
1045 return $block_content;
1046 }
1047
1048 $global_settings = gutenberg_get_global_settings();
1049 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
1050 if ( empty( $fallback_layout ) ) {
1051 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
1052 }
1053 $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
1054
1055 $class_names = array();
1056 $layout_definitions = gutenberg_get_layout_definitions();
1057
1058 // Set the correct layout type for blocks using legacy content width.
1059 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
1060 $used_layout['type'] = 'constrained';
1061 }
1062
1063 $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
1064
1065 if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) {
1066 $class_names[] = 'has-global-padding';
1067 }
1068
1069 /*
1070 * The following section was added to reintroduce a small set of layout classnames that were
1071 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
1072 * not intended to provide an extended set of classes to match all block layout attributes
1073 * here.
1074 */
1075 if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
1076 $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
1077 }
1078
1079 if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
1080 $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
1081 }
1082
1083 if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
1084 $class_names[] = 'is-nowrap';
1085 }
1086
1087 // Get classname for layout type.
1088 if ( isset( $used_layout['type'] ) ) {
1089 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
1090 } else {
1091 $layout_classname = $layout_definitions['default']['className'] ?? '';
1092 }
1093
1094 if ( $layout_classname && is_string( $layout_classname ) ) {
1095 $class_names[] = sanitize_title( $layout_classname );
1096 }
1097
1098 /*
1099 * Only generate Layout styles if the theme has not opted-out.
1100 * Attribute-based Layout classnames are output in all cases.
1101 */
1102 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
1103
1104 $gap_value = gutenberg_sanitize_block_gap_value( $block['attrs']['style']['spacing']['blockGap'] ?? null );
1105
1106 $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
1107 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
1108
1109 /*
1110 * If a block's block.json skips serialization for spacing or spacing.blockGap,
1111 * don't apply the user-defined value to the styles.
1112 */
1113 $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
1114
1115 $block_gap = $global_settings['spacing']['blockGap'] ?? null;
1116 $has_block_gap_support = isset( $block_gap );
1117
1118 // Get default blockGap value from global styles for use in layouts like grid.
1119 // Check style variation first, then block-specific styles, then fall back to root styles.
1120 $block_name = $block['blockName'] ?? '';
1121 if ( null === $global_styles ) {
1122 $global_styles = gutenberg_get_global_styles();
1123 }
1124
1125 // Check if the block has an active style variation with a blockGap value.
1126 // Only check the registry if the className contains a variation class to avoid unnecessary lookups.
1127 $variation_block_gap_value = null;
1128 $block_class_name = $block['attrs']['className'] ?? '';
1129 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1130 $styles_registry = WP_Block_Styles_Registry::get_instance();
1131 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1132 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
1133 if ( $variation_name ) {
1134 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1135 }
1136 }
1137
1138 $global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;
1139
1140 if ( null !== $global_block_gap_value ) {
1141 $fallback_gap_value = $global_block_gap_value;
1142 }
1143
1144 /*
1145 * We generate a unique ID based on all the data required to obtain the
1146 * corresponding layout style. This way, the CSS class names keep the same
1147 * even for different blocks with the same layout definition. We need this to
1148 * make the CSS class names stable across paginations for features like the
1149 * enhanced pagination of the Query block.
1150 */
1151 $container_class_hash_input = array(
1152 $used_layout,
1153 $has_block_gap_support,
1154 $gap_value,
1155 $should_skip_gap_serialization,
1156 $fallback_gap_value,
1157 $block_spacing,
1158 );
1159
1160 foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
1161 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1162 if ( ! is_array( $viewport_style ) ) {
1163 continue;
1164 }
1165
1166 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1167 if ( ! empty( $viewport_container_layout ) ) {
1168 $container_class_hash_input[] = array(
1169 'breakpoint' => $breakpoint,
1170 'layout' => $viewport_container_layout,
1171 );
1172 }
1173
1174 if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
1175 $container_class_hash_input[] = array(
1176 'breakpoint' => $breakpoint,
1177 'blockGap' => gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
1178 );
1179 }
1180 }
1181
1182 $container_class = gutenberg_unique_id_from_values(
1183 $container_class_hash_input,
1184 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
1185 );
1186
1187 $style = gutenberg_get_layout_style(
1188 ".$container_class",
1189 $used_layout,
1190 $has_block_gap_support,
1191 $gap_value,
1192 $should_skip_gap_serialization,
1193 $fallback_gap_value,
1194 $block_spacing
1195 );
1196
1197 // Only add container class and enqueue block support styles if unique styles were generated.
1198 if ( ! empty( $style ) ) {
1199 $class_names[] = $container_class;
1200 }
1201
1202 /*
1203 * Emit responsive container layout styles using the same $container_class
1204 * selector as the base layout so they target the inner block wrapper.
1205 */
1206 foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1207 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1208 if ( ! is_array( $viewport_style ) ) {
1209 continue;
1210 }
1211
1212 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1213 $has_viewport_layout = ! empty( $viewport_container_layout );
1214 $has_viewport_block_gap = isset( $viewport_style['spacing']['blockGap'] );
1215
1216 if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
1217 continue;
1218 }
1219
1220 $viewport_gap_value = $has_viewport_block_gap
1221 ? gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
1222 : $gap_value;
1223 $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
1224 ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
1225 : $block_spacing;
1226
1227 $viewport_styles = gutenberg_get_layout_style(
1228 ".$container_class",
1229 $used_layout,
1230 $has_block_gap_support,
1231 $viewport_gap_value,
1232 $should_skip_gap_serialization,
1233 $fallback_gap_value,
1234 $viewport_block_spacing,
1235 array(
1236 'rules_group' => $media_query,
1237 'viewport_overrides' => $viewport_container_layout,
1238 'has_block_gap_override' => $has_viewport_block_gap,
1239 )
1240 );
1241
1242 if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
1243 $class_names[] = $container_class;
1244 }
1245 }
1246 }
1247
1248 // Add combined layout and block classname for global styles to hook onto.
1249 $split_block_name = explode( '/', $block['blockName'] );
1250 $full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
1251 $class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
1252
1253 // Add classes to the outermost HTML tag if necessary.
1254 if ( ! empty( $outer_class_names ) ) {
1255 foreach ( $outer_class_names as $outer_class_name ) {
1256 $processor->add_class( $outer_class_name );
1257 }
1258 }
1259
1260 /*
1261 * Attempts to refer to the inner-block wrapping element by its class attribute.
1262 *
1263 * When examining a block's inner content, if a block has inner blocks, then
1264 * the first content item will likely be a text (HTML) chunk immediately
1265 * preceding the inner blocks. The last HTML tag in that chunk would then be
1266 * an opening tag for an element that wraps the inner blocks.
1267 *
1268 * There's no reliable way to associate this wrapper in $block_content because
1269 * it may have changed during the rendering pipeline (as inner contents is
1270 * provided before rendering) and through previous filters. In many cases,
1271 * however, the `class` attribute will be a good-enough identifier, so this
1272 * code finds the last tag in that chunk and stores the `class` attribute
1273 * so that it can be used later when working through the rendered block output
1274 * to identify the wrapping element and add the remaining class names to it.
1275 *
1276 * It's also possible that no inner block wrapper even exists. If that's the
1277 * case this code could apply the class names to an invalid element.
1278 *
1279 * Example:
1280 *
1281 * $block['innerBlocks'] = array( $list_item );
1282 * $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
1283 *
1284 * // After rendering, the initial contents may have been modified by other renderers or filters.
1285 * $block_content = <<<HTML
1286 * <figure>
1287 * <ul class="annotated-list list-wrapper is-unordered">
1288 * <li>Code</li>
1289 * </ul><figcaption>It's a list!</figcaption>
1290 * </figure>
1291 * HTML;
1292 *
1293 * Although it is possible that the original block-wrapper classes are changed in $block_content
1294 * from how they appear in $block['innerContent'], it's likely that the original class attributes
1295 * are still present in the wrapper as they are in this example. Frequently, additional classes
1296 * will also be present; rarely should classes be removed.
1297 *
1298 * @todo Find a better way to match the first inner block. If it's possible to identify where the
1299 * first inner block starts, then it will be possible to find the last tag before it starts
1300 * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
1301 * Can some unique value or class or ID be added to the inner blocks when they process
1302 * so that they can be extracted here safely without guessing? Can the block rendering function
1303 * return information about where the rendered inner blocks start?
1304 *
1305 * @var string|null
1306 */
1307 $inner_block_wrapper_classes = null;
1308 $first_chunk = $block['innerContent'][0] ?? null;
1309 if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
1310 $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
1311 /*
1312 * Use a stack to track open elements as tags are visited. Void elements
1313 * (those without a matching closing tag) are excluded so they don't
1314 * accumulate on the stack. At the end of the chunk, every element still
1315 * on the stack is unclosed — meaning its closing tag lives in a later
1316 * innerContent entry alongside the inner blocks, which makes it the
1317 * inner-block container. Elements that open and close within this chunk
1318 * are siblings that precede the inner blocks and should be ignored.
1319 * The last unclosed element with a class attribute is the best candidate
1320 * for the inner-block wrapper.
1321 */
1322 $tag_stack = array();
1323 while ( $first_chunk_processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
1324 if ( $first_chunk_processor->is_tag_closer() ) {
1325 array_pop( $tag_stack );
1326 } elseif ( ! WP_HTML_Processor::is_void( $first_chunk_processor->get_tag() ) ) {
1327 $tag_stack[] = $first_chunk_processor->get_attribute( 'class' );
1328 }
1329 }
1330 foreach ( array_reverse( $tag_stack ) as $class_attribute ) {
1331 if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
1332 $inner_block_wrapper_classes = $class_attribute;
1333 break;
1334 }
1335 }
1336 }
1337
1338 /*
1339 * If necessary, advance to what is likely to be an inner block wrapper tag.
1340 *
1341 * This advances until it finds the first tag containing the original class
1342 * attribute from above. If none is found it will scan to the end of the block
1343 * and fail to add any class names.
1344 *
1345 * If there is no block wrapper it won't advance at all, in which case the
1346 * class names will be added to the first and outermost tag of the block.
1347 * For cases where this outermost tag is the only tag surrounding inner
1348 * blocks then the outer wrapper and inner wrapper are the same.
1349 */
1350 do {
1351 if ( ! $inner_block_wrapper_classes ) {
1352 break;
1353 }
1354
1355 $class_attribute = $processor->get_attribute( 'class' );
1356 if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
1357 break;
1358 }
1359 } while ( $processor->next_tag() );
1360
1361 // Add the remaining class names.
1362 foreach ( $class_names as $class_name ) {
1363 $processor->add_class( $class_name );
1364 }
1365
1366 return $processor->get_updated_html();
1367 }
1368
1369 /*
1370 * Add a `render_block_data` filter to fetch the parent block layout data.
1371 */
1372 add_filter(
1373 'render_block_data',
1374 function ( $parsed_block, $source_block, $parent_block ) {
1375 /*
1376 * Check if the parent block exists and if it has a layout attribute.
1377 * If it does, add the parent layout to the parsed block.
1378 */
1379 if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) {
1380 $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout'];
1381 }
1382 return $parsed_block;
1383 },
1384 10,
1385 3
1386 );
1387
1388 // Register the block support. (overrides core one).
1389 WP_Block_Supports::get_instance()->register(
1390 'layout',
1391 array(
1392 'register_attribute' => 'gutenberg_register_layout_support',
1393 )
1394 );
1395
1396 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
1397 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
1398 }
1399 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
1400
1401 /**
1402 * For themes without theme.json file, make sure
1403 * to restore the inner div for the group block
1404 * to avoid breaking styles relying on that div.
1405 *
1406 * @param string $block_content Rendered block content.
1407 * @param array $block Block object.
1408 * @return string Filtered block content.
1409 */
1410 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1411 $tag_name = $block['attrs']['tagName'] ?? 'div';
1412 $group_with_inner_container_regex = sprintf(
1413 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1414 preg_quote( $tag_name, '/' )
1415 );
1416 if (
1417 wp_theme_has_theme_json() ||
1418 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
1419 ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
1420 ) {
1421 return $block_content;
1422 }
1423
1424 /*
1425 * This filter runs after the layout classnames have been added to the block, so they
1426 * have to be removed from the outer wrapper and then added to the inner.
1427 */
1428 $layout_classes = array();
1429 $processor = new WP_HTML_Tag_Processor( $block_content );
1430
1431 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
1432 foreach ( $processor->class_list() as $class_name ) {
1433 if ( str_contains( $class_name, 'layout' ) ) {
1434 array_push( $layout_classes, $class_name );
1435 $processor->remove_class( $class_name );
1436 }
1437 }
1438 }
1439
1440 $content_without_layout_classes = $processor->get_updated_html();
1441 $replace_regex = sprintf(
1442 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
1443 preg_quote( $tag_name, '/' )
1444 );
1445 $updated_content = preg_replace_callback(
1446 $replace_regex,
1447 static function ( $matches ) {
1448 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
1449 },
1450 $content_without_layout_classes
1451 );
1452
1453 // Add layout classes to inner wrapper.
1454 if ( ! empty( $layout_classes ) ) {
1455 $processor = new WP_HTML_Tag_Processor( $updated_content );
1456 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) {
1457 foreach ( $layout_classes as $class_name ) {
1458 $processor->add_class( $class_name );
1459 }
1460 }
1461 $updated_content = $processor->get_updated_html();
1462 }
1463
1464 return $updated_content;
1465 }
1466
1467 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
1468 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10 );
1469 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10 );
1470 }
1471 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
1472
1473 /**
1474 * For themes without theme.json file, make sure
1475 * to restore the outer div for the aligned image block
1476 * to avoid breaking styles relying on that div.
1477 *
1478 * @param string $block_content Rendered block content.
1479 * @param array $block Block object.
1480 * @return string Filtered block content.
1481 */
1482 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1483 if ( wp_theme_has_theme_json() ) {
1484 return $block_content;
1485 }
1486
1487 $figure_processor = new WP_HTML_Tag_Processor( $block_content );
1488 if (
1489 ! $figure_processor->next_tag( 'FIGURE' ) ||
1490 ! $figure_processor->has_class( 'wp-block-image' ) ||
1491 ! (
1492 $figure_processor->has_class( 'alignleft' ) ||
1493 $figure_processor->has_class( 'aligncenter' ) ||
1494 $figure_processor->has_class( 'alignright' )
1495 )
1496 ) {
1497 return $block_content;
1498 }
1499
1500 /*
1501 * The next section of code wraps the existing figure in a new DIV element.
1502 * While doing it, it needs to transfer the layout and the additional CSS
1503 * class names from the original figure upward to the wrapper.
1504 *
1505 * Example:
1506 *
1507 * // From this…
1508 * <!-- wp:image {"className":"hires"} -->
1509 * <figure class="wp-block-image wide hires">…
1510 *
1511 * // To this…
1512 * <div class="wp-block-image hires"><figure class="wide">…
1513 */
1514 $wrapper_processor = new WP_HTML_Tag_Processor( '<div>' );
1515 $wrapper_processor->next_token();
1516 $wrapper_processor->set_attribute(
1517 'class',
1518 is_string( $block['attrs']['className'] ?? null )
1519 ? "wp-block-image {$block['attrs']['className']}"
1520 : 'wp-block-image'
1521 );
1522
1523 // And remove them from the existing content; it has been transferred upward.
1524 $figure_processor->remove_class( 'wp-block-image' );
1525 foreach ( $wrapper_processor->class_list() as $class_name ) {
1526 $figure_processor->remove_class( $class_name );
1527 }
1528
1529 return "{$wrapper_processor->get_updated_html()}{$figure_processor->get_updated_html()}</div>";
1530 }
1531
1532 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
1533 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10 );
1534 }
1535 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
1536