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

1,538 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 = is_string( $block['attrs']['className'] ?? null )
1129 ? $block['attrs']['className']
1130 : '';
1131 if ( $block_class_name && str_contains( $block_class_name, 'is-style-' ) && $block_name ) {
1132 $styles_registry = WP_Block_Styles_Registry::get_instance();
1133 $registered_styles = $styles_registry->get_registered_styles_for_block( $block_name );
1134 $variation_name = gutenberg_get_block_style_variation_name_from_registered_style( $block_class_name, $registered_styles );
1135 if ( $variation_name ) {
1136 $variation_block_gap_value = $global_styles['blocks'][ $block_name ]['variations'][ $variation_name ]['spacing']['blockGap'] ?? null;
1137 }
1138 }
1139
1140 $global_block_gap_value = $variation_block_gap_value ?? $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? $global_styles['spacing']['blockGap'] ?? null;
1141
1142 if ( null !== $global_block_gap_value ) {
1143 $fallback_gap_value = $global_block_gap_value;
1144 }
1145
1146 /*
1147 * We generate a unique ID based on all the data required to obtain the
1148 * corresponding layout style. This way, the CSS class names keep the same
1149 * even for different blocks with the same layout definition. We need this to
1150 * make the CSS class names stable across paginations for features like the
1151 * enhanced pagination of the Query block.
1152 */
1153 $container_class_hash_input = array(
1154 $used_layout,
1155 $has_block_gap_support,
1156 $gap_value,
1157 $should_skip_gap_serialization,
1158 $fallback_gap_value,
1159 $block_spacing,
1160 );
1161
1162 foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
1163 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1164 if ( ! is_array( $viewport_style ) ) {
1165 continue;
1166 }
1167
1168 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1169 if ( ! empty( $viewport_container_layout ) ) {
1170 $container_class_hash_input[] = array(
1171 'breakpoint' => $breakpoint,
1172 'layout' => $viewport_container_layout,
1173 );
1174 }
1175
1176 if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
1177 $container_class_hash_input[] = array(
1178 'breakpoint' => $breakpoint,
1179 'blockGap' => gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
1180 );
1181 }
1182 }
1183
1184 $container_class = gutenberg_unique_id_from_values(
1185 $container_class_hash_input,
1186 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
1187 );
1188
1189 $style = gutenberg_get_layout_style(
1190 ".$container_class",
1191 $used_layout,
1192 $has_block_gap_support,
1193 $gap_value,
1194 $should_skip_gap_serialization,
1195 $fallback_gap_value,
1196 $block_spacing
1197 );
1198
1199 // Only add container class and enqueue block support styles if unique styles were generated.
1200 if ( ! empty( $style ) ) {
1201 $class_names[] = $container_class;
1202 }
1203
1204 /*
1205 * Emit responsive container layout styles using the same $container_class
1206 * selector as the base layout so they target the inner block wrapper.
1207 */
1208 foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
1209 $viewport_style = $style_attr[ $breakpoint ] ?? null;
1210 if ( ! is_array( $viewport_style ) ) {
1211 continue;
1212 }
1213
1214 $viewport_container_layout = gutenberg_get_layout_container_values( $viewport_style['layout'] ?? null );
1215 $has_viewport_layout = ! empty( $viewport_container_layout );
1216 $has_viewport_block_gap = isset( $viewport_style['spacing']['blockGap'] );
1217
1218 if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
1219 continue;
1220 }
1221
1222 $viewport_gap_value = $has_viewport_block_gap
1223 ? gutenberg_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
1224 : $gap_value;
1225 $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
1226 ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
1227 : $block_spacing;
1228
1229 $viewport_styles = gutenberg_get_layout_style(
1230 ".$container_class",
1231 $used_layout,
1232 $has_block_gap_support,
1233 $viewport_gap_value,
1234 $should_skip_gap_serialization,
1235 $fallback_gap_value,
1236 $viewport_block_spacing,
1237 array(
1238 'rules_group' => $media_query,
1239 'viewport_overrides' => $viewport_container_layout,
1240 'has_block_gap_override' => $has_viewport_block_gap,
1241 )
1242 );
1243
1244 if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
1245 $class_names[] = $container_class;
1246 }
1247 }
1248 }
1249
1250 // Add combined layout and block classname for global styles to hook onto.
1251 $split_block_name = explode( '/', $block['blockName'] );
1252 $full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
1253 $class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
1254
1255 // Add classes to the outermost HTML tag if necessary.
1256 if ( ! empty( $outer_class_names ) ) {
1257 foreach ( $outer_class_names as $outer_class_name ) {
1258 $processor->add_class( $outer_class_name );
1259 }
1260 }
1261
1262 /*
1263 * Attempts to refer to the inner-block wrapping element by its class attribute.
1264 *
1265 * When examining a block's inner content, if a block has inner blocks, then
1266 * the first content item will likely be a text (HTML) chunk immediately
1267 * preceding the inner blocks. The last HTML tag in that chunk would then be
1268 * an opening tag for an element that wraps the inner blocks.
1269 *
1270 * There's no reliable way to associate this wrapper in $block_content because
1271 * it may have changed during the rendering pipeline (as inner contents is
1272 * provided before rendering) and through previous filters. In many cases,
1273 * however, the `class` attribute will be a good-enough identifier, so this
1274 * code finds the last tag in that chunk and stores the `class` attribute
1275 * so that it can be used later when working through the rendered block output
1276 * to identify the wrapping element and add the remaining class names to it.
1277 *
1278 * It's also possible that no inner block wrapper even exists. If that's the
1279 * case this code could apply the class names to an invalid element.
1280 *
1281 * Example:
1282 *
1283 * $block['innerBlocks'] = array( $list_item );
1284 * $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
1285 *
1286 * // After rendering, the initial contents may have been modified by other renderers or filters.
1287 * $block_content = <<<HTML
1288 * <figure>
1289 * <ul class="annotated-list list-wrapper is-unordered">
1290 * <li>Code</li>
1291 * </ul><figcaption>It's a list!</figcaption>
1292 * </figure>
1293 * HTML;
1294 *
1295 * Although it is possible that the original block-wrapper classes are changed in $block_content
1296 * from how they appear in $block['innerContent'], it's likely that the original class attributes
1297 * are still present in the wrapper as they are in this example. Frequently, additional classes
1298 * will also be present; rarely should classes be removed.
1299 *
1300 * @todo Find a better way to match the first inner block. If it's possible to identify where the
1301 * first inner block starts, then it will be possible to find the last tag before it starts
1302 * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
1303 * Can some unique value or class or ID be added to the inner blocks when they process
1304 * so that they can be extracted here safely without guessing? Can the block rendering function
1305 * return information about where the rendered inner blocks start?
1306 *
1307 * @var string|null
1308 */
1309 $inner_block_wrapper_classes = null;
1310 $first_chunk = $block['innerContent'][0] ?? null;
1311 if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
1312 $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
1313 /*
1314 * Use a stack to track open elements as tags are visited. Void elements
1315 * (those without a matching closing tag) are excluded so they don't
1316 * accumulate on the stack. At the end of the chunk, every element still
1317 * on the stack is unclosed — meaning its closing tag lives in a later
1318 * innerContent entry alongside the inner blocks, which makes it the
1319 * inner-block container. Elements that open and close within this chunk
1320 * are siblings that precede the inner blocks and should be ignored.
1321 * The last unclosed element with a class attribute is the best candidate
1322 * for the inner-block wrapper.
1323 */
1324 $tag_stack = array();
1325 while ( $first_chunk_processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
1326 if ( $first_chunk_processor->is_tag_closer() ) {
1327 array_pop( $tag_stack );
1328 } elseif ( ! WP_HTML_Processor::is_void( $first_chunk_processor->get_tag() ) ) {
1329 $tag_stack[] = $first_chunk_processor->get_attribute( 'class' );
1330 }
1331 }
1332 foreach ( array_reverse( $tag_stack ) as $class_attribute ) {
1333 if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
1334 $inner_block_wrapper_classes = $class_attribute;
1335 break;
1336 }
1337 }
1338 }
1339
1340 /*
1341 * If necessary, advance to what is likely to be an inner block wrapper tag.
1342 *
1343 * This advances until it finds the first tag containing the original class
1344 * attribute from above. If none is found it will scan to the end of the block
1345 * and fail to add any class names.
1346 *
1347 * If there is no block wrapper it won't advance at all, in which case the
1348 * class names will be added to the first and outermost tag of the block.
1349 * For cases where this outermost tag is the only tag surrounding inner
1350 * blocks then the outer wrapper and inner wrapper are the same.
1351 */
1352 do {
1353 if ( ! $inner_block_wrapper_classes ) {
1354 break;
1355 }
1356
1357 $class_attribute = $processor->get_attribute( 'class' );
1358 if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
1359 break;
1360 }
1361 } while ( $processor->next_tag() );
1362
1363 // Add the remaining class names.
1364 foreach ( $class_names as $class_name ) {
1365 $processor->add_class( $class_name );
1366 }
1367
1368 return $processor->get_updated_html();
1369 }
1370
1371 /*
1372 * Add a `render_block_data` filter to fetch the parent block layout data.
1373 */
1374 add_filter(
1375 'render_block_data',
1376 function ( $parsed_block, $source_block, $parent_block ) {
1377 /*
1378 * Check if the parent block exists and if it has a layout attribute.
1379 * If it does, add the parent layout to the parsed block.
1380 */
1381 if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) {
1382 $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout'];
1383 }
1384 return $parsed_block;
1385 },
1386 10,
1387 3
1388 );
1389
1390 // Register the block support. (overrides core one).
1391 WP_Block_Supports::get_instance()->register(
1392 'layout',
1393 array(
1394 'register_attribute' => 'gutenberg_register_layout_support',
1395 )
1396 );
1397
1398 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
1399 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
1400 }
1401 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
1402
1403 /**
1404 * For themes without theme.json file, make sure
1405 * to restore the inner div for the group block
1406 * to avoid breaking styles relying on that div.
1407 *
1408 * @param string $block_content Rendered block content.
1409 * @param array $block Block object.
1410 * @return string Filtered block content.
1411 */
1412 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1413 $tag_name = $block['attrs']['tagName'] ?? 'div';
1414 $group_with_inner_container_regex = sprintf(
1415 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1416 preg_quote( $tag_name, '/' )
1417 );
1418 if (
1419 wp_theme_has_theme_json() ||
1420 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
1421 ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
1422 ) {
1423 return $block_content;
1424 }
1425
1426 /*
1427 * This filter runs after the layout classnames have been added to the block, so they
1428 * have to be removed from the outer wrapper and then added to the inner.
1429 */
1430 $layout_classes = array();
1431 $processor = new WP_HTML_Tag_Processor( $block_content );
1432
1433 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
1434 foreach ( $processor->class_list() as $class_name ) {
1435 if ( str_contains( $class_name, 'layout' ) ) {
1436 array_push( $layout_classes, $class_name );
1437 $processor->remove_class( $class_name );
1438 }
1439 }
1440 }
1441
1442 $content_without_layout_classes = $processor->get_updated_html();
1443 $replace_regex = sprintf(
1444 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
1445 preg_quote( $tag_name, '/' )
1446 );
1447 $updated_content = preg_replace_callback(
1448 $replace_regex,
1449 static function ( $matches ) {
1450 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
1451 },
1452 $content_without_layout_classes
1453 );
1454
1455 // Add layout classes to inner wrapper.
1456 if ( ! empty( $layout_classes ) ) {
1457 $processor = new WP_HTML_Tag_Processor( $updated_content );
1458 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) {
1459 foreach ( $layout_classes as $class_name ) {
1460 $processor->add_class( $class_name );
1461 }
1462 }
1463 $updated_content = $processor->get_updated_html();
1464 }
1465
1466 return $updated_content;
1467 }
1468
1469 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
1470 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10 );
1471 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10 );
1472 }
1473 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
1474
1475 /**
1476 * For themes without theme.json file, make sure
1477 * to restore the outer div for the aligned image block
1478 * to avoid breaking styles relying on that div.
1479 *
1480 * @param string $block_content Rendered block content.
1481 * @param array $block Block object.
1482 * @return string Filtered block content.
1483 */
1484 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1485 if ( wp_theme_has_theme_json() ) {
1486 return $block_content;
1487 }
1488
1489 $figure_processor = new WP_HTML_Tag_Processor( $block_content );
1490 if (
1491 ! $figure_processor->next_tag( 'FIGURE' ) ||
1492 ! $figure_processor->has_class( 'wp-block-image' ) ||
1493 ! (
1494 $figure_processor->has_class( 'alignleft' ) ||
1495 $figure_processor->has_class( 'aligncenter' ) ||
1496 $figure_processor->has_class( 'alignright' )
1497 )
1498 ) {
1499 return $block_content;
1500 }
1501
1502 /*
1503 * The next section of code wraps the existing figure in a new DIV element.
1504 * While doing it, it needs to transfer the layout and the additional CSS
1505 * class names from the original figure upward to the wrapper.
1506 *
1507 * Example:
1508 *
1509 * // From this…
1510 * <!-- wp:image {"className":"hires"} -->
1511 * <figure class="wp-block-image wide hires">…
1512 *
1513 * // To this…
1514 * <div class="wp-block-image hires"><figure class="wide">…
1515 */
1516 $wrapper_processor = new WP_HTML_Tag_Processor( '<div>' );
1517 $wrapper_processor->next_token();
1518 $wrapper_processor->set_attribute(
1519 'class',
1520 is_string( $block['attrs']['className'] ?? null )
1521 ? "wp-block-image {$block['attrs']['className']}"
1522 : 'wp-block-image'
1523 );
1524
1525 // And remove them from the existing content; it has been transferred upward.
1526 $figure_processor->remove_class( 'wp-block-image' );
1527 foreach ( $wrapper_processor->class_list() as $class_name ) {
1528 $figure_processor->remove_class( $class_name );
1529 }
1530
1531 return "{$wrapper_processor->get_updated_html()}{$figure_processor->get_updated_html()}</div>";
1532 }
1533
1534 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
1535 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10 );
1536 }
1537 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
1538