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

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

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