PluginProbe
Gutenberg / 23.7.2
Gutenberg v23.7.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / block-supports / layout.php

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

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