PluginProbe
Gutenberg / 19.6.1
Gutenberg v19.6.1
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 19.6.1, at lib/block-supports/layout.php

1,124 lines 40.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 * Returns layout definitions, keyed by layout type.
10 *
11 * Provides a common definition of slugs, classnames, base styles, and spacing styles for each layout type.
12 * When making changes or additions to layout definitions, the corresponding JavaScript definitions should
13 * also be updated.
14 *
15 * @return array[] Layout definitions.
16 */
17 function gutenberg_get_layout_definitions() {
18 $layout_definitions = array(
19 'default' => array(
20 'name' => 'default',
21 'slug' => 'flow',
22 'className' => 'is-layout-flow',
23 'baseStyles' => array(
24 array(
25 'selector' => ' > .alignleft',
26 'rules' => array(
27 'float' => 'left',
28 'margin-inline-start' => '0',
29 'margin-inline-end' => '2em',
30 ),
31 ),
32 array(
33 'selector' => ' > .alignright',
34 'rules' => array(
35 'float' => 'right',
36 'margin-inline-start' => '2em',
37 'margin-inline-end' => '0',
38 ),
39 ),
40 array(
41 'selector' => ' > .aligncenter',
42 'rules' => array(
43 'margin-left' => 'auto !important',
44 'margin-right' => 'auto !important',
45 ),
46 ),
47 ),
48 'spacingStyles' => array(
49 array(
50 'selector' => ' > :first-child',
51 'rules' => array(
52 'margin-block-start' => '0',
53 ),
54 ),
55 array(
56 'selector' => ' > :last-child',
57 'rules' => array(
58 'margin-block-end' => '0',
59 ),
60 ),
61 array(
62 'selector' => ' > *',
63 'rules' => array(
64 'margin-block-start' => null,
65 'margin-block-end' => '0',
66 ),
67 ),
68 ),
69 ),
70 'constrained' => array(
71 'name' => 'constrained',
72 'slug' => 'constrained',
73 'className' => 'is-layout-constrained',
74 'baseStyles' => array(
75 array(
76 'selector' => ' > .alignleft',
77 'rules' => array(
78 'float' => 'left',
79 'margin-inline-start' => '0',
80 'margin-inline-end' => '2em',
81 ),
82 ),
83 array(
84 'selector' => ' > .alignright',
85 'rules' => array(
86 'float' => 'right',
87 'margin-inline-start' => '2em',
88 'margin-inline-end' => '0',
89 ),
90 ),
91 array(
92 'selector' => ' > .aligncenter',
93 'rules' => array(
94 'margin-left' => 'auto !important',
95 'margin-right' => 'auto !important',
96 ),
97 ),
98 array(
99 'selector' => ' > :where(:not(.alignleft):not(.alignright):not(.alignfull))',
100 'rules' => array(
101 'max-width' => 'var(--wp--style--global--content-size)',
102 'margin-left' => 'auto !important',
103 'margin-right' => 'auto !important',
104 ),
105 ),
106 array(
107 'selector' => ' > .alignwide',
108 'rules' => array(
109 'max-width' => 'var(--wp--style--global--wide-size)',
110 ),
111 ),
112 ),
113 'spacingStyles' => array(
114 array(
115 'selector' => ' > :first-child',
116 'rules' => array(
117 'margin-block-start' => '0',
118 ),
119 ),
120 array(
121 'selector' => ' > :last-child',
122 'rules' => array(
123 'margin-block-end' => '0',
124 ),
125 ),
126 array(
127 'selector' => ' > *',
128 'rules' => array(
129 'margin-block-start' => null,
130 'margin-block-end' => '0',
131 ),
132 ),
133 ),
134 ),
135 'flex' => array(
136 'name' => 'flex',
137 'slug' => 'flex',
138 'className' => 'is-layout-flex',
139 'displayMode' => 'flex',
140 'baseStyles' => array(
141 array(
142 'selector' => '',
143 'rules' => array(
144 'flex-wrap' => 'wrap',
145 'align-items' => 'center',
146 ),
147 ),
148 array(
149 'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
150 'rules' => array(
151 'margin' => '0',
152 ),
153 ),
154 ),
155 'spacingStyles' => array(
156 array(
157 'selector' => '',
158 'rules' => array(
159 'gap' => null,
160 ),
161 ),
162 ),
163 ),
164 'grid' => array(
165 'name' => 'grid',
166 'slug' => 'grid',
167 'className' => 'is-layout-grid',
168 'displayMode' => 'grid',
169 'baseStyles' => array(
170 array(
171 'selector' => ' > :is(*, div)', // :is(*, div) instead of just * increases the specificity by 001.
172 'rules' => array(
173 'margin' => '0',
174 ),
175 ),
176 ),
177 'spacingStyles' => array(
178 array(
179 'selector' => '',
180 'rules' => array(
181 'gap' => null,
182 ),
183 ),
184 ),
185 ),
186 );
187
188 return $layout_definitions;
189 }
190
191 /**
192 * Registers the layout block attribute for block types that support it.
193 *
194 * @param WP_Block_Type $block_type Block Type.
195 */
196 function gutenberg_register_layout_support( $block_type ) {
197 $support_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
198 if ( $support_layout ) {
199 if ( ! $block_type->attributes ) {
200 $block_type->attributes = array();
201 }
202
203 if ( ! array_key_exists( 'layout', $block_type->attributes ) ) {
204 $block_type->attributes['layout'] = array(
205 'type' => 'object',
206 );
207 }
208 }
209 }
210
211 /**
212 * Generates the CSS corresponding to the provided layout.
213 *
214 * @param string $selector CSS selector.
215 * @param array $layout Layout object. The one that is passed has already checked
216 * the existence of default block layout.
217 * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
218 * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null.
219 * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
220 * @param string $fallback_gap_value Optional. The block gap value to apply. Default '0.5em'.
221 * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
222 * @return string CSS styles on success. Else, empty string.
223 */
224 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 ) {
225 $layout_type = $layout['type'] ?? 'default';
226 $layout_styles = array();
227
228 if ( 'default' === $layout_type ) {
229 if ( $has_block_gap_support ) {
230 if ( is_array( $gap_value ) ) {
231 $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
232 }
233 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
234 // Get spacing CSS variable from preset value if provided.
235 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
236 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
237 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
238 $gap_value = "var(--wp--preset--spacing--$slug)";
239 }
240
241 array_push(
242 $layout_styles,
243 array(
244 'selector' => "$selector > *",
245 'declarations' => array(
246 'margin-block-start' => '0',
247 'margin-block-end' => '0',
248 ),
249 ),
250 array(
251 'selector' => "$selector > * + *",
252 'declarations' => array(
253 'margin-block-start' => $gap_value,
254 'margin-block-end' => '0',
255 ),
256 )
257 );
258 }
259 }
260 } elseif ( 'constrained' === $layout_type ) {
261 $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : '';
262 $wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : '';
263 $justify_content = isset( $layout['justifyContent'] ) ? $layout['justifyContent'] : 'center';
264
265 $all_max_width_value = $content_size ? $content_size : $wide_size;
266 $wide_max_width_value = $wide_size ? $wide_size : $content_size;
267
268 // Make sure there is a single CSS rule, and all tags are stripped for security.
269 $all_max_width_value = safecss_filter_attr( explode( ';', $all_max_width_value )[0] );
270 $wide_max_width_value = safecss_filter_attr( explode( ';', $wide_max_width_value )[0] );
271
272 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
273 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
274
275 if ( $content_size || $wide_size ) {
276 array_push(
277 $layout_styles,
278 array(
279 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
280 'declarations' => array(
281 'max-width' => $all_max_width_value,
282 'margin-left' => $margin_left,
283 'margin-right' => $margin_right,
284 ),
285 ),
286 array(
287 'selector' => "$selector > .alignwide",
288 'declarations' => array( 'max-width' => $wide_max_width_value ),
289 ),
290 array(
291 'selector' => "$selector .alignfull",
292 'declarations' => array( 'max-width' => 'none' ),
293 )
294 );
295 }
296
297 if ( isset( $block_spacing ) ) {
298 $block_spacing_values = gutenberg_style_engine_get_styles(
299 array(
300 'spacing' => $block_spacing,
301 )
302 );
303
304 /*
305 * Handle negative margins for alignfull children of blocks with custom padding set.
306 * They're added separately because padding might only be set on one side.
307 */
308 if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
309 $padding_right = $block_spacing_values['declarations']['padding-right'];
310 // Add unit if 0.
311 if ( '0' === $padding_right ) {
312 $padding_right = '0px';
313 }
314 $layout_styles[] = array(
315 'selector' => "$selector > .alignfull",
316 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
317 );
318 }
319 if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
320 $padding_left = $block_spacing_values['declarations']['padding-left'];
321 // Add unit if 0.
322 if ( '0' === $padding_left ) {
323 $padding_left = '0px';
324 }
325 $layout_styles[] = array(
326 'selector' => "$selector > .alignfull",
327 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
328 );
329 }
330 }
331
332 if ( 'left' === $justify_content ) {
333 $layout_styles[] = array(
334 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
335 'declarations' => array( 'margin-left' => '0 !important' ),
336 );
337 }
338
339 if ( 'right' === $justify_content ) {
340 $layout_styles[] = array(
341 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
342 'declarations' => array( 'margin-right' => '0 !important' ),
343 );
344 }
345
346 if ( $has_block_gap_support ) {
347 if ( is_array( $gap_value ) ) {
348 $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
349 }
350 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
351 // Get spacing CSS variable from preset value if provided.
352 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
353 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
354 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
355 $gap_value = "var(--wp--preset--spacing--$slug)";
356 }
357
358 array_push(
359 $layout_styles,
360 array(
361 'selector' => "$selector > *",
362 'declarations' => array(
363 'margin-block-start' => '0',
364 'margin-block-end' => '0',
365 ),
366 ),
367 array(
368 'selector' => "$selector > * + *",
369 'declarations' => array(
370 'margin-block-start' => $gap_value,
371 'margin-block-end' => '0',
372 ),
373 )
374 );
375 }
376 }
377 } elseif ( 'flex' === $layout_type ) {
378 $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal';
379
380 $justify_content_options = array(
381 'left' => 'flex-start',
382 'right' => 'flex-end',
383 'center' => 'center',
384 );
385
386 $vertical_alignment_options = array(
387 'top' => 'flex-start',
388 'center' => 'center',
389 'bottom' => 'flex-end',
390 );
391
392 if ( 'horizontal' === $layout_orientation ) {
393 $justify_content_options += array( 'space-between' => 'space-between' );
394 $vertical_alignment_options += array( 'stretch' => 'stretch' );
395 } else {
396 $justify_content_options += array( 'stretch' => 'stretch' );
397 $vertical_alignment_options += array( 'space-between' => 'space-between' );
398 }
399
400 if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
401 $layout_styles[] = array(
402 'selector' => $selector,
403 'declarations' => array( 'flex-wrap' => 'nowrap' ),
404 );
405 }
406
407 if ( $has_block_gap_support && isset( $gap_value ) ) {
408 $combined_gap_value = '';
409 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
410
411 foreach ( $gap_sides as $gap_side ) {
412 $process_value = $gap_value;
413 if ( is_array( $gap_value ) ) {
414 $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
415 }
416 // Get spacing CSS variable from preset value if provided.
417 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
418 $index_to_splice = strrpos( $process_value, '|' ) + 1;
419 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
420 $process_value = "var(--wp--preset--spacing--$slug)";
421 }
422 $combined_gap_value .= "$process_value ";
423 }
424 $gap_value = trim( $combined_gap_value );
425
426 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
427 $layout_styles[] = array(
428 'selector' => $selector,
429 'declarations' => array( 'gap' => $gap_value ),
430 );
431 }
432 }
433
434 if ( 'horizontal' === $layout_orientation ) {
435 /*
436 * Add this style only if is not empty for backwards compatibility,
437 * since we intend to convert blocks that had flex layout implemented
438 * by custom css.
439 */
440 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
441 $layout_styles[] = array(
442 'selector' => $selector,
443 'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ),
444 );
445 }
446
447 if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
448 $layout_styles[] = array(
449 'selector' => $selector,
450 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
451 );
452 }
453 } else {
454 $layout_styles[] = array(
455 'selector' => $selector,
456 'declarations' => array( 'flex-direction' => 'column' ),
457 );
458 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
459 $layout_styles[] = array(
460 'selector' => $selector,
461 'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ),
462 );
463 } else {
464 $layout_styles[] = array(
465 'selector' => $selector,
466 'declarations' => array( 'align-items' => 'flex-start' ),
467 );
468 }
469 if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
470 $layout_styles[] = array(
471 'selector' => $selector,
472 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
473 );
474 }
475 }
476 } elseif ( 'grid' === $layout_type ) {
477 // Deal with block gap first so it can be used for responsive computation.
478 $responsive_gap_value = '1.2rem';
479 if ( $has_block_gap_support && isset( $gap_value ) ) {
480 $combined_gap_value = '';
481 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
482
483 foreach ( $gap_sides as $gap_side ) {
484 $process_value = $gap_value;
485 if ( is_array( $gap_value ) ) {
486 $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
487 }
488 // Get spacing CSS variable from preset value if provided.
489 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
490 $index_to_splice = strrpos( $process_value, '|' ) + 1;
491 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
492 $process_value = "var(--wp--preset--spacing--$slug)";
493 }
494 $combined_gap_value .= "$process_value ";
495 }
496 $gap_value = trim( $combined_gap_value );
497 $responsive_gap_value = $gap_value;
498 }
499
500 if ( ! empty( $layout['columnCount'] ) && ! empty( $layout['minimumColumnWidth'] ) ) {
501 $max_value = 'max(' . $layout['minimumColumnWidth'] . ', (100% - (' . $responsive_gap_value . ' * (' . $layout['columnCount'] . ' - 1))) /' . $layout['columnCount'] . ')';
502 $layout_styles[] = array(
503 'selector' => $selector,
504 'declarations' => array(
505 'grid-template-columns' => 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))',
506 'container-type' => 'inline-size',
507 ),
508 );
509 if ( ! empty( $layout['rowCount'] ) ) {
510 $layout_styles[] = array(
511 'selector' => $selector,
512 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
513 );
514 }
515 } elseif ( ! empty( $layout['columnCount'] ) ) {
516 $layout_styles[] = array(
517 'selector' => $selector,
518 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ),
519 );
520 if ( ! empty( $layout['rowCount'] ) ) {
521 $layout_styles[] = array(
522 'selector' => $selector,
523 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
524 );
525 }
526 } else {
527 $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem';
528
529 $layout_styles[] = array(
530 'selector' => $selector,
531 'declarations' => array(
532 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))',
533 'container-type' => 'inline-size',
534 ),
535 );
536 }
537
538 if ( $has_block_gap_support && null !== $gap_value && ! $should_skip_gap_serialization ) {
539 $layout_styles[] = array(
540 'selector' => $selector,
541 'declarations' => array( 'gap' => $gap_value ),
542 );
543 }
544 }
545
546 if ( ! empty( $layout_styles ) ) {
547 /*
548 * Add to the style engine store to enqueue and render layout styles.
549 * Return compiled layout styles to retain backwards compatibility.
550 * Since https://github.com/WordPress/gutenberg/pull/42452,
551 * wp_enqueue_block_support_styles is no longer called in this block supports file.
552 */
553 return gutenberg_style_engine_get_stylesheet_from_css_rules(
554 $layout_styles,
555 array(
556 'context' => 'block-supports',
557 'prettify' => false,
558 )
559 );
560 }
561
562 return '';
563 }
564
565 /**
566 * Generates an incremental ID that is independent per each different prefix.
567 *
568 * It is similar to `wp_unique_id`, but each prefix has it's own internal ID
569 * counter to make each prefix independent from each other. The ID starts at 1
570 * and increments on each call. The returned value is not universally unique,
571 * but it is unique across the life of the PHP process and it's stable per
572 * prefix.
573 *
574 * @param string $prefix Prefix for the returned ID.
575 * @return string Incremental ID per prefix.
576 */
577 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
578 static $id_counters = array();
579 if ( ! array_key_exists( $prefix, $id_counters ) ) {
580 $id_counters[ $prefix ] = 0;
581 }
582 return $prefix . (string) ++$id_counters[ $prefix ];
583 }
584
585 /**
586 * Renders the layout config to the block wrapper.
587 *
588 * @param string $block_content Rendered block content.
589 * @param array $block Block object.
590 * @return string Filtered block content.
591 */
592 function gutenberg_render_layout_support_flag( $block_content, $block ) {
593 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
594 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
595 // If there is any value in style -> layout, the block has a child layout.
596 $child_layout = $block['attrs']['style']['layout'] ?? null;
597
598 if ( ! $block_supports_layout && ! $child_layout ) {
599 return $block_content;
600 }
601
602 $outer_class_names = array();
603
604 // Child layout specific logic.
605 if ( $child_layout ) {
606 $container_content_class = wp_unique_prefixed_id( 'wp-container-content-' );
607 $child_layout_declarations = array();
608 $child_layout_styles = array();
609
610 $self_stretch = isset( $block['attrs']['style']['layout']['selfStretch'] ) ? $block['attrs']['style']['layout']['selfStretch'] : null;
611
612 if ( 'fixed' === $self_stretch && isset( $block['attrs']['style']['layout']['flexSize'] ) ) {
613 $child_layout_declarations['flex-basis'] = $block['attrs']['style']['layout']['flexSize'];
614 $child_layout_declarations['box-sizing'] = 'border-box';
615 } elseif ( 'fill' === $self_stretch ) {
616 $child_layout_declarations['flex-grow'] = '1';
617 }
618
619 $column_start = isset( $block['attrs']['style']['layout']['columnStart'] ) ? $block['attrs']['style']['layout']['columnStart'] : null;
620 $column_span = isset( $block['attrs']['style']['layout']['columnSpan'] ) ? $block['attrs']['style']['layout']['columnSpan'] : null;
621 if ( $column_start && $column_span ) {
622 $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
623 } elseif ( $column_start ) {
624 $child_layout_declarations['grid-column'] = "$column_start";
625 } elseif ( $column_span ) {
626 $child_layout_declarations['grid-column'] = "span $column_span";
627 }
628
629 $row_start = isset( $block['attrs']['style']['layout']['rowStart'] ) ? $block['attrs']['style']['layout']['rowStart'] : null;
630 $row_span = isset( $block['attrs']['style']['layout']['rowSpan'] ) ? $block['attrs']['style']['layout']['rowSpan'] : null;
631 if ( $row_start && $row_span ) {
632 $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
633 } elseif ( $row_start ) {
634 $child_layout_declarations['grid-row'] = "$row_start";
635 } elseif ( $row_span ) {
636 $child_layout_declarations['grid-row'] = "span $row_span";
637 }
638
639 $child_layout_styles[] = array(
640 'selector' => ".$container_content_class",
641 'declarations' => $child_layout_declarations,
642 );
643
644 $minimum_column_width = isset( $block['parentLayout']['minimumColumnWidth'] ) ? $block['parentLayout']['minimumColumnWidth'] : null;
645 $column_count = isset( $block['parentLayout']['columnCount'] ) ? $block['parentLayout']['columnCount'] : null;
646
647 /*
648 * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
649 * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
650 * once the grid has less columns than the start.
651 * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
652 * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
653 */
654 if ( ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
655 $column_span_number = floatval( $column_span );
656 $column_start_number = floatval( $column_start );
657 $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
658 $parent_column_value = floatval( $parent_column_width );
659 $parent_column_unit = explode( $parent_column_value, $parent_column_width );
660
661 $num_cols_to_break_at = 2;
662 if ( $column_span_number && $column_start_number ) {
663 $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
664 } elseif ( $column_span_number ) {
665 $num_cols_to_break_at = $column_span_number;
666 } else {
667 $num_cols_to_break_at = $column_start_number;
668 }
669
670 /*
671 * If there is no unit, the width has somehow been mangled so we reset both unit and value
672 * to defaults.
673 * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
674 */
675 if ( count( $parent_column_unit ) <= 1 ) {
676 $parent_column_unit = 'rem';
677 $parent_column_value = 12;
678 } else {
679 $parent_column_unit = $parent_column_unit[1];
680
681 if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
682 $parent_column_unit = 'rem';
683 }
684 }
685
686 /*
687 * A default gap value is used for this computation because custom gap values may not be
688 * viable to use in the computation of the container query value.
689 */
690 $default_gap_value = 'px' === $parent_column_unit ? 24 : 1.5;
691 $container_query_value = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
692 $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
693 $container_query_value = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
694 // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
695 $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
696
697 $child_layout_styles[] = array(
698 'rules_group' => "@container (max-width: $container_query_value )",
699 'selector' => ".$container_content_class",
700 'declarations' => array(
701 'grid-column' => $grid_column_value,
702 'grid-row' => 'auto',
703 ),
704 );
705 }
706
707 /*
708 * Add to the style engine store to enqueue and render layout styles.
709 * Return styles here just to check if any exist.
710 */
711 $child_css = gutenberg_style_engine_get_stylesheet_from_css_rules(
712 $child_layout_styles,
713 array(
714 'context' => 'block-supports',
715 'prettify' => false,
716 )
717 );
718
719 if ( $child_css ) {
720 $outer_class_names[] = $container_content_class;
721 }
722 }
723
724 // Prep the processor for modifying the block output.
725 $processor = new WP_HTML_Tag_Processor( $block_content );
726
727 // Having no tags implies there are no tags onto which to add class names.
728 if ( ! $processor->next_tag() ) {
729 return $block_content;
730 }
731
732 /*
733 * A block may not support layout but still be affected by a parent block's layout.
734 *
735 * In these cases add the appropriate class names and then return early; there's
736 * no need to investigate on this block whether additional layout constraints apply.
737 */
738 if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
739 foreach ( $outer_class_names as $class_name ) {
740 $processor->add_class( $class_name );
741 }
742 return $processor->get_updated_html();
743 } elseif ( ! $block_supports_layout ) {
744 // Ensure layout classnames are not injected if there is no layout support.
745 return $block_content;
746 }
747
748 $global_settings = gutenberg_get_global_settings();
749 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
750 if ( empty( $fallback_layout ) ) {
751 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
752 }
753 $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
754
755 $class_names = array();
756 $layout_definitions = gutenberg_get_layout_definitions();
757
758 /*
759 * We use an incremental ID that is independent per prefix to make sure that
760 * rendering different numbers of blocks doesn't affect the IDs of other
761 * blocks. We need this to make the CSS class names stable across paginations
762 * for features like the enhanced pagination of the Query block.
763 */
764 $container_class = gutenberg_incremental_id_per_prefix(
765 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
766 );
767
768 // Set the correct layout type for blocks using legacy content width.
769 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
770 $used_layout['type'] = 'constrained';
771 }
772
773 $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
774
775 if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) {
776 $class_names[] = 'has-global-padding';
777 }
778
779 /*
780 * The following section was added to reintroduce a small set of layout classnames that were
781 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
782 * not intended to provide an extended set of classes to match all block layout attributes
783 * here.
784 */
785 if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
786 $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
787 }
788
789 if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
790 $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
791 }
792
793 if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
794 $class_names[] = 'is-nowrap';
795 }
796
797 // Get classname for layout type.
798 if ( isset( $used_layout['type'] ) ) {
799 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
800 } else {
801 $layout_classname = $layout_definitions['default']['className'] ?? '';
802 }
803
804 if ( $layout_classname && is_string( $layout_classname ) ) {
805 $class_names[] = sanitize_title( $layout_classname );
806 }
807
808 /*
809 * Only generate Layout styles if the theme has not opted-out.
810 * Attribute-based Layout classnames are output in all cases.
811 */
812 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
813
814 $gap_value = $block['attrs']['style']['spacing']['blockGap'] ?? null;
815
816 /*
817 * Skip if gap value contains unsupported characters.
818 * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
819 * to only match against the value, not the CSS attribute.
820 */
821 if ( is_array( $gap_value ) ) {
822 foreach ( $gap_value as $key => $value ) {
823 $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
824 }
825 } else {
826 $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
827 }
828
829 $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
830 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
831
832 /*
833 * If a block's block.json skips serialization for spacing or spacing.blockGap,
834 * don't apply the user-defined value to the styles.
835 */
836 $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
837
838 $block_gap = $global_settings['spacing']['blockGap'] ?? null;
839 $has_block_gap_support = isset( $block_gap );
840
841 $style = gutenberg_get_layout_style(
842 ".$container_class",
843 $used_layout,
844 $has_block_gap_support,
845 $gap_value,
846 $should_skip_gap_serialization,
847 $fallback_gap_value,
848 $block_spacing
849 );
850
851 // Only add container class and enqueue block support styles if unique styles were generated.
852 if ( ! empty( $style ) ) {
853 $class_names[] = $container_class;
854 }
855 }
856
857 // Add combined layout and block classname for global styles to hook onto.
858 $split_block_name = explode( '/', $block['blockName'] );
859 $full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
860 $class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
861
862 // Add classes to the outermost HTML tag if necessary.
863 if ( ! empty( $outer_class_names ) ) {
864 foreach ( $outer_class_names as $outer_class_name ) {
865 $processor->add_class( $outer_class_name );
866 }
867 }
868
869 /*
870 * Attempts to refer to the inner-block wrapping element by its class attribute.
871 *
872 * When examining a block's inner content, if a block has inner blocks, then
873 * the first content item will likely be a text (HTML) chunk immediately
874 * preceding the inner blocks. The last HTML tag in that chunk would then be
875 * an opening tag for an element that wraps the inner blocks.
876 *
877 * There's no reliable way to associate this wrapper in $block_content because
878 * it may have changed during the rendering pipeline (as inner contents is
879 * provided before rendering) and through previous filters. In many cases,
880 * however, the `class` attribute will be a good-enough identifier, so this
881 * code finds the last tag in that chunk and stores the `class` attribute
882 * so that it can be used later when working through the rendered block output
883 * to identify the wrapping element and add the remaining class names to it.
884 *
885 * It's also possible that no inner block wrapper even exists. If that's the
886 * case this code could apply the class names to an invalid element.
887 *
888 * Example:
889 *
890 * $block['innerBlocks'] = array( $list_item );
891 * $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
892 *
893 * // After rendering, the initial contents may have been modified by other renderers or filters.
894 * $block_content = <<<HTML
895 * <figure>
896 * <ul class="annotated-list list-wrapper is-unordered">
897 * <li>Code</li>
898 * </ul><figcaption>It's a list!</figcaption>
899 * </figure>
900 * HTML;
901 *
902 * Although it is possible that the original block-wrapper classes are changed in $block_content
903 * from how they appear in $block['innerContent'], it's likely that the original class attributes
904 * are still present in the wrapper as they are in this example. Frequently, additional classes
905 * will also be present; rarely should classes be removed.
906 *
907 * @todo Find a better way to match the first inner block. If it's possible to identify where the
908 * first inner block starts, then it will be possible to find the last tag before it starts
909 * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
910 * Can some unique value or class or ID be added to the inner blocks when they process
911 * so that they can be extracted here safely without guessing? Can the block rendering function
912 * return information about where the rendered inner blocks start?
913 *
914 * @var string|null
915 */
916 $inner_block_wrapper_classes = null;
917 $first_chunk = $block['innerContent'][0] ?? null;
918 if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
919 $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
920 while ( $first_chunk_processor->next_tag() ) {
921 $class_attribute = $first_chunk_processor->get_attribute( 'class' );
922 if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
923 $inner_block_wrapper_classes = $class_attribute;
924 }
925 }
926 }
927
928 /*
929 * If necessary, advance to what is likely to be an inner block wrapper tag.
930 *
931 * This advances until it finds the first tag containing the original class
932 * attribute from above. If none is found it will scan to the end of the block
933 * and fail to add any class names.
934 *
935 * If there is no block wrapper it won't advance at all, in which case the
936 * class names will be added to the first and outermost tag of the block.
937 * For cases where this outermost tag is the only tag surrounding inner
938 * blocks then the outer wrapper and inner wrapper are the same.
939 */
940 do {
941 if ( ! $inner_block_wrapper_classes ) {
942 break;
943 }
944
945 $class_attribute = $processor->get_attribute( 'class' );
946 if ( is_string( $class_attribute ) && str_contains( $class_attribute, $inner_block_wrapper_classes ) ) {
947 break;
948 }
949 } while ( $processor->next_tag() );
950
951 // Add the remaining class names.
952 foreach ( $class_names as $class_name ) {
953 $processor->add_class( $class_name );
954 }
955
956 return $processor->get_updated_html();
957 }
958
959 /*
960 * Add a `render_block_data` filter to fetch the parent block layout data.
961 */
962 add_filter(
963 'render_block_data',
964 function ( $parsed_block, $source_block, $parent_block ) {
965 /*
966 * Check if the parent block exists and if it has a layout attribute.
967 * If it does, add the parent layout to the parsed block.
968 */
969 if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) {
970 $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout'];
971 }
972 return $parsed_block;
973 },
974 10,
975 3
976 );
977
978 // Register the block support. (overrides core one).
979 WP_Block_Supports::get_instance()->register(
980 'layout',
981 array(
982 'register_attribute' => 'gutenberg_register_layout_support',
983 )
984 );
985
986 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
987 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
988 }
989 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
990
991 /**
992 * For themes without theme.json file, make sure
993 * to restore the inner div for the group block
994 * to avoid breaking styles relying on that div.
995 *
996 * @param string $block_content Rendered block content.
997 * @param array $block Block object.
998 * @return string Filtered block content.
999 */
1000 function gutenberg_restore_group_inner_container( $block_content, $block ) {
1001 $tag_name = isset( $block['attrs']['tagName'] ) ? $block['attrs']['tagName'] : 'div';
1002 $group_with_inner_container_regex = sprintf(
1003 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
1004 preg_quote( $tag_name, '/' )
1005 );
1006 if (
1007 wp_theme_has_theme_json() ||
1008 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
1009 ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
1010 ) {
1011 return $block_content;
1012 }
1013
1014 /*
1015 * This filter runs after the layout classnames have been added to the block, so they
1016 * have to be removed from the outer wrapper and then added to the inner.
1017 */
1018 $layout_classes = array();
1019 $processor = new WP_HTML_Tag_Processor( $block_content );
1020
1021 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
1022 foreach ( $processor->class_list() as $class_name ) {
1023 if ( str_contains( $class_name, 'layout' ) ) {
1024 array_push( $layout_classes, $class_name );
1025 $processor->remove_class( $class_name );
1026 }
1027 }
1028 }
1029
1030 $content_without_layout_classes = $processor->get_updated_html();
1031 $replace_regex = sprintf(
1032 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
1033 preg_quote( $tag_name, '/' )
1034 );
1035 $updated_content = preg_replace_callback(
1036 $replace_regex,
1037 static function ( $matches ) {
1038 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
1039 },
1040 $content_without_layout_classes
1041 );
1042
1043 // Add layout classes to inner wrapper.
1044 if ( ! empty( $layout_classes ) ) {
1045 $processor = new WP_HTML_Tag_Processor( $updated_content );
1046 if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) {
1047 foreach ( $layout_classes as $class_name ) {
1048 $processor->add_class( $class_name );
1049 }
1050 }
1051 $updated_content = $processor->get_updated_html();
1052 }
1053
1054 return $updated_content;
1055 }
1056
1057 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
1058 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10, 2 );
1059 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10, 2 );
1060 }
1061 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
1062
1063
1064 /**
1065 * For themes without theme.json file, make sure
1066 * to restore the outer div for the aligned image block
1067 * to avoid breaking styles relying on that div.
1068 *
1069 * @param string $block_content Rendered block content.
1070 * @param array $block Block object.
1071 * @return string Filtered block content.
1072 */
1073 function gutenberg_restore_image_outer_container( $block_content, $block ) {
1074 $image_with_align = "
1075 /# 1) everything up to the class attribute contents
1076 (
1077 ^\s*
1078 <figure\b
1079 [^>]*
1080 \bclass=
1081 [\"']
1082 )
1083 # 2) the class attribute contents
1084 (
1085 [^\"']*
1086 \bwp-block-image\b
1087 [^\"']*
1088 \b(?:alignleft|alignright|aligncenter)\b
1089 [^\"']*
1090 )
1091 # 3) everything after the class attribute contents
1092 (
1093 [\"']
1094 [^>]*
1095 >
1096 .*
1097 <\/figure>
1098 )/iUx";
1099
1100 if (
1101 wp_theme_has_theme_json() ||
1102 0 === preg_match( $image_with_align, $block_content, $matches )
1103 ) {
1104 return $block_content;
1105 }
1106
1107 $wrapper_classnames = array( 'wp-block-image' );
1108
1109 // If the block has a classNames attribute these classnames need to be removed from the content and added back
1110 // to the new wrapper div also.
1111 if ( ! empty( $block['attrs']['className'] ) ) {
1112 $wrapper_classnames = array_merge( $wrapper_classnames, explode( ' ', $block['attrs']['className'] ) );
1113 }
1114 $content_classnames = explode( ' ', $matches[2] );
1115 $filtered_content_classnames = array_diff( $content_classnames, $wrapper_classnames );
1116
1117 return '<div class="' . implode( ' ', $wrapper_classnames ) . '">' . $matches[1] . implode( ' ', $filtered_content_classnames ) . $matches[3] . '</div>';
1118 }
1119
1120 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
1121 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10, 2 );
1122 }
1123 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
1124