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

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