PluginProbe
Gutenberg / 17.0.2
Gutenberg v17.0.2
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 17.0.2, at lib/block-supports/layout.php

949 lines 32.4 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:first-child',
51 'rules' => array(
52 'margin-block-start' => '0',
53 ),
54 ),
55 array(
56 'selector' => ' > :last-child: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:first-child',
116 'rules' => array(
117 'margin-block-start' => '0',
118 ),
119 ),
120 array(
121 'selector' => ' > :last-child: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' => ' > *',
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' => ' > *',
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$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 if ( isset( $block_spacing ) ) {
297 $block_spacing_values = gutenberg_style_engine_get_styles(
298 array(
299 'spacing' => $block_spacing,
300 )
301 );
302
303 /*
304 * Handle negative margins for alignfull children of blocks with custom padding set.
305 * They're added separately because padding might only be set on one side.
306 */
307 if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
308 $padding_right = $block_spacing_values['declarations']['padding-right'];
309 $layout_styles[] = array(
310 'selector' => "$selector > .alignfull",
311 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
312 );
313 }
314 if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
315 $padding_left = $block_spacing_values['declarations']['padding-left'];
316 $layout_styles[] = array(
317 'selector' => "$selector > .alignfull",
318 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
319 );
320 }
321 }
322 }
323
324 if ( 'left' === $justify_content ) {
325 $layout_styles[] = array(
326 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
327 'declarations' => array( 'margin-left' => '0 !important' ),
328 );
329 }
330
331 if ( 'right' === $justify_content ) {
332 $layout_styles[] = array(
333 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
334 'declarations' => array( 'margin-right' => '0 !important' ),
335 );
336 }
337
338 if ( $has_block_gap_support ) {
339 if ( is_array( $gap_value ) ) {
340 $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
341 }
342 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
343 // Get spacing CSS variable from preset value if provided.
344 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
345 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
346 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
347 $gap_value = "var(--wp--preset--spacing--$slug)";
348 }
349
350 array_push(
351 $layout_styles,
352 array(
353 'selector' => "$selector > *",
354 'declarations' => array(
355 'margin-block-start' => '0',
356 'margin-block-end' => '0',
357 ),
358 ),
359 array(
360 'selector' => "$selector$selector > * + *",
361 'declarations' => array(
362 'margin-block-start' => $gap_value,
363 'margin-block-end' => '0',
364 ),
365 )
366 );
367 }
368 }
369 } elseif ( 'flex' === $layout_type ) {
370 $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal';
371
372 $justify_content_options = array(
373 'left' => 'flex-start',
374 'right' => 'flex-end',
375 'center' => 'center',
376 );
377
378 $vertical_alignment_options = array(
379 'top' => 'flex-start',
380 'center' => 'center',
381 'bottom' => 'flex-end',
382 );
383
384 if ( 'horizontal' === $layout_orientation ) {
385 $justify_content_options += array( 'space-between' => 'space-between' );
386 $vertical_alignment_options += array( 'stretch' => 'stretch' );
387 } else {
388 $justify_content_options += array( 'stretch' => 'stretch' );
389 $vertical_alignment_options += array( 'space-between' => 'space-between' );
390 }
391
392 if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
393 $layout_styles[] = array(
394 'selector' => $selector,
395 'declarations' => array( 'flex-wrap' => 'nowrap' ),
396 );
397 }
398
399 if ( $has_block_gap_support && isset( $gap_value ) ) {
400 $combined_gap_value = '';
401 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
402
403 foreach ( $gap_sides as $gap_side ) {
404 $process_value = $gap_value;
405 if ( is_array( $gap_value ) ) {
406 $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
407 }
408 // Get spacing CSS variable from preset value if provided.
409 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
410 $index_to_splice = strrpos( $process_value, '|' ) + 1;
411 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
412 $process_value = "var(--wp--preset--spacing--$slug)";
413 }
414 $combined_gap_value .= "$process_value ";
415 }
416 $gap_value = trim( $combined_gap_value );
417
418 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
419 $layout_styles[] = array(
420 'selector' => $selector,
421 'declarations' => array( 'gap' => $gap_value ),
422 );
423 }
424 }
425
426 if ( 'horizontal' === $layout_orientation ) {
427 /*
428 * Add this style only if is not empty for backwards compatibility,
429 * since we intend to convert blocks that had flex layout implemented
430 * by custom css.
431 */
432 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
433 $layout_styles[] = array(
434 'selector' => $selector,
435 'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ),
436 );
437 }
438
439 if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
440 $layout_styles[] = array(
441 'selector' => $selector,
442 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
443 );
444 }
445 } else {
446 $layout_styles[] = array(
447 'selector' => $selector,
448 'declarations' => array( 'flex-direction' => 'column' ),
449 );
450 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
451 $layout_styles[] = array(
452 'selector' => $selector,
453 'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ),
454 );
455 } else {
456 $layout_styles[] = array(
457 'selector' => $selector,
458 'declarations' => array( 'align-items' => 'flex-start' ),
459 );
460 }
461 if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
462 $layout_styles[] = array(
463 'selector' => $selector,
464 'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
465 );
466 }
467 }
468 } elseif ( 'grid' === $layout_type ) {
469 if ( ! empty( $layout['columnCount'] ) ) {
470 $layout_styles[] = array(
471 'selector' => $selector,
472 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ),
473 );
474 } else {
475 $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem';
476
477 $layout_styles[] = array(
478 'selector' => $selector,
479 'declarations' => array( 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))' ),
480 );
481 }
482
483 if ( $has_block_gap_support && isset( $gap_value ) ) {
484 $combined_gap_value = '';
485 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
486
487 foreach ( $gap_sides as $gap_side ) {
488 $process_value = $gap_value;
489 if ( is_array( $gap_value ) ) {
490 $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
491 }
492 // Get spacing CSS variable from preset value if provided.
493 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
494 $index_to_splice = strrpos( $process_value, '|' ) + 1;
495 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
496 $process_value = "var(--wp--preset--spacing--$slug)";
497 }
498 $combined_gap_value .= "$process_value ";
499 }
500 $gap_value = trim( $combined_gap_value );
501
502 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
503 $layout_styles[] = array(
504 'selector' => $selector,
505 'declarations' => array( 'gap' => $gap_value ),
506 );
507 }
508 }
509 }
510
511 if ( ! empty( $layout_styles ) ) {
512 /*
513 * Add to the style engine store to enqueue and render layout styles.
514 * Return compiled layout styles to retain backwards compatibility.
515 * Since https://github.com/WordPress/gutenberg/pull/42452,
516 * wp_enqueue_block_support_styles is no longer called in this block supports file.
517 */
518 return gutenberg_style_engine_get_stylesheet_from_css_rules(
519 $layout_styles,
520 array(
521 'context' => 'block-supports',
522 'prettify' => false,
523 )
524 );
525 }
526
527 return '';
528 }
529
530 /**
531 * Generates an incremental ID that is independent per each different prefix.
532 *
533 * It is similar to `wp_unique_id`, but each prefix has it's own internal ID
534 * counter to make each prefix independent from each other. The ID starts at 1
535 * and increments on each call. The returned value is not universally unique,
536 * but it is unique across the life of the PHP process and it's stable per
537 * prefix.
538 *
539 * @param string $prefix Prefix for the returned ID.
540 * @return string Incremental ID per prefix.
541 */
542 function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
543 static $id_counters = array();
544 if ( ! array_key_exists( $prefix, $id_counters ) ) {
545 $id_counters[ $prefix ] = 0;
546 }
547 return $prefix . (string) ++$id_counters[ $prefix ];
548 }
549
550 /**
551 * Renders the layout config to the block wrapper.
552 *
553 * @param string $block_content Rendered block content.
554 * @param array $block Block object.
555 * @return string Filtered block content.
556 */
557 function gutenberg_render_layout_support_flag( $block_content, $block ) {
558 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
559 $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
560 $layout_from_parent = $block['attrs']['style']['layout']['selfStretch'] ?? null;
561
562 if ( ! $block_supports_layout && ! $layout_from_parent ) {
563 return $block_content;
564 }
565
566 $outer_class_names = array();
567
568 if ( 'fixed' === $layout_from_parent || 'fill' === $layout_from_parent ) {
569 $container_content_class = wp_unique_id( 'wp-container-content-' );
570
571 $child_layout_styles = array();
572
573 if ( 'fixed' === $layout_from_parent && isset( $block['attrs']['style']['layout']['flexSize'] ) ) {
574 $child_layout_styles[] = array(
575 'selector' => ".$container_content_class",
576 'declarations' => array(
577 'flex-basis' => $block['attrs']['style']['layout']['flexSize'],
578 'box-sizing' => 'border-box',
579 ),
580 );
581 } elseif ( 'fill' === $layout_from_parent ) {
582 $child_layout_styles[] = array(
583 'selector' => ".$container_content_class",
584 'declarations' => array(
585 'flex-grow' => '1',
586 ),
587 );
588 }
589
590 gutenberg_style_engine_get_stylesheet_from_css_rules(
591 $child_layout_styles,
592 array(
593 'context' => 'block-supports',
594 'prettify' => false,
595 )
596 );
597
598 $outer_class_names[] = $container_content_class;
599 }
600
601 // Prep the processor for modifying the block output.
602 $processor = new WP_HTML_Tag_Processor( $block_content );
603
604 // Having no tags implies there are no tags onto which to add class names.
605 if ( ! $processor->next_tag() ) {
606 return $block_content;
607 }
608
609 /*
610 * A block may not support layout but still be affected by a parent block's layout.
611 *
612 * In these cases add the appropriate class names and then return early; there's
613 * no need to investigate on this block whether additional layout constraints apply.
614 */
615 if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
616 foreach ( $outer_class_names as $class_name ) {
617 $processor->add_class( $class_name );
618 }
619 return $processor->get_updated_html();
620 }
621
622 $global_settings = gutenberg_get_global_settings();
623 $fallback_layout = $block_type->supports['layout']['default'] ?? array();
624 if ( empty( $fallback_layout ) ) {
625 $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
626 }
627 $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
628
629 $class_names = array();
630 $layout_definitions = gutenberg_get_layout_definitions();
631
632 /*
633 * We use an incremental ID that is independent per prefix to make sure that
634 * rendering different numbers of blocks doesn't affect the IDs of other
635 * blocks. We need this to make the CSS class names stable across paginations
636 * for features like the enhanced pagination of the Query block.
637 */
638 $container_class = gutenberg_incremental_id_per_prefix(
639 'wp-container-' . sanitize_title( $block['blockName'] ) . '-layout-'
640 );
641
642 // Set the correct layout type for blocks using legacy content width.
643 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
644 $used_layout['type'] = 'constrained';
645 }
646
647 $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
648
649 if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) {
650 $class_names[] = 'has-global-padding';
651 }
652
653 /*
654 * The following section was added to reintroduce a small set of layout classnames that were
655 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
656 * not intended to provide an extended set of classes to match all block layout attributes
657 * here.
658 */
659 if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
660 $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
661 }
662
663 if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
664 $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
665 }
666
667 if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
668 $class_names[] = 'is-nowrap';
669 }
670
671 // Get classname for layout type.
672 if ( isset( $used_layout['type'] ) ) {
673 $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
674 } else {
675 $layout_classname = $layout_definitions['default']['className'] ?? '';
676 }
677
678 if ( $layout_classname && is_string( $layout_classname ) ) {
679 $class_names[] = sanitize_title( $layout_classname );
680 }
681
682 /*
683 * Only generate Layout styles if the theme has not opted-out.
684 * Attribute-based Layout classnames are output in all cases.
685 */
686 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
687
688 $gap_value = $block['attrs']['style']['spacing']['blockGap'] ?? null;
689
690 /*
691 * Skip if gap value contains unsupported characters.
692 * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
693 * to only match against the value, not the CSS attribute.
694 */
695 if ( is_array( $gap_value ) ) {
696 foreach ( $gap_value as $key => $value ) {
697 $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
698 }
699 } else {
700 $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
701 }
702
703 $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
704 $block_spacing = $block['attrs']['style']['spacing'] ?? null;
705
706 /*
707 * If a block's block.json skips serialization for spacing or spacing.blockGap,
708 * don't apply the user-defined value to the styles.
709 */
710 $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
711
712 $block_gap = $global_settings['spacing']['blockGap'] ?? null;
713 $has_block_gap_support = isset( $block_gap );
714
715 $style = gutenberg_get_layout_style(
716 ".$container_class.$container_class",
717 $used_layout,
718 $has_block_gap_support,
719 $gap_value,
720 $should_skip_gap_serialization,
721 $fallback_gap_value,
722 $block_spacing
723 );
724
725 // Only add container class and enqueue block support styles if unique styles were generated.
726 if ( ! empty( $style ) ) {
727 $class_names[] = $container_class;
728 }
729 }
730
731 // Add combined layout and block classname for global styles to hook onto.
732 $split_block_name = explode( '/', $block['blockName'] );
733 $full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
734 $class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
735
736 // Add classes to the outermost HTML tag if necessary.
737 if ( ! empty( $outer_class_names ) ) {
738 foreach ( $outer_class_names as $outer_class_name ) {
739 $processor->add_class( $outer_class_name );
740 }
741 }
742
743 /**
744 * Attempts to refer to the inner-block wrapping element by its class attribute.
745 *
746 * When examining a block's inner content, if a block has inner blocks, then
747 * the first content item will likely be a text (HTML) chunk immediately
748 * preceding the inner blocks. The last HTML tag in that chunk would then be
749 * an opening tag for an element that wraps the inner blocks.
750 *
751 * There's no reliable way to associate this wrapper in $block_content because
752 * it may have changed during the rendering pipeline (as inner contents is
753 * provided before rendering) and through previous filters. In many cases,
754 * however, the `class` attribute will be a good-enough identifier, so this
755 * code finds the last tag in that chunk and stores the `class` attribute
756 * so that it can be used later when working through the rendered block output
757 * to identify the wrapping element and add the remaining class names to it.
758 *
759 * It's also possible that no inner block wrapper even exists. If that's the
760 * case this code could apply the class names to an invalid element.
761 *
762 * Example:
763 *
764 * $block['innerBlocks'] = array( $list_item );
765 * $block['innerContent'] = array( '<ul class="list-wrapper is-unordered">', null, '</ul>' );
766 *
767 * // After rendering, the initial contents may have been modified by other renderers or filters.
768 * $block_content = <<<HTML
769 * <figure>
770 * <ul class="annotated-list list-wrapper is-unordered">
771 * <li>Code</li>
772 * </ul><figcaption>It's a list!</figcaption>
773 * </figure>
774 * HTML;
775 *
776 * Although it is possible that the original block-wrapper classes are changed in $block_content
777 * from how they appear in $block['innerContent'], it's likely that the original class attributes
778 * are still present in the wrapper as they are in this example. Frequently, additional classes
779 * will also be present; rarely should classes be removed.
780 *
781 * @TODO: Find a better way to match the first inner block. If it's possible to identify where the
782 * first inner block starts, then it will be possible to find the last tag before it starts
783 * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
784 * Can some unique value or class or ID be added to the inner blocks when they process
785 * so that they can be extracted here safely without guessing? Can the block rendering function
786 * return information about where the rendered inner blocks start?
787 *
788 * @var string|null
789 */
790 $inner_block_wrapper_classes = null;
791 $first_chunk = $block['innerContent'][0] ?? null;
792 if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
793 $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
794 while ( $first_chunk_processor->next_tag() ) {
795 $class_attribute = $first_chunk_processor->get_attribute( 'class' );
796 if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
797 $inner_block_wrapper_classes = $class_attribute;
798 }
799 }
800 }
801
802 /*
803 * If necessary, advance to what is likely to be an inner block wrapper tag.
804 *
805 * This advances until it finds the first tag containing the original class
806 * attribute from above. If none is found it will scan to the end of the block
807 * and fail to add any class names.
808 *
809 * If there is no block wrapper it won't advance at all, in which case the
810 * class names will be added to the first and outermost tag of the block.
811 * For cases where this outermost tag is the only tag surrounding inner
812 * blocks then the outer wrapper and inner wrapper are the same.
813 */
814 do {
815 if ( ! $inner_block_wrapper_classes ) {
816 break;
817 }
818
819 if ( false !== strpos( $processor->get_attribute( 'class' ), $inner_block_wrapper_classes ) ) {
820 break;
821 }
822 } while ( $processor->next_tag() );
823
824 // Add the remaining class names.
825 foreach ( $class_names as $class_name ) {
826 $processor->add_class( $class_name );
827 }
828
829 return $processor->get_updated_html();
830 }
831
832 // Register the block support. (overrides core one).
833 WP_Block_Supports::get_instance()->register(
834 'layout',
835 array(
836 'register_attribute' => 'gutenberg_register_layout_support',
837 )
838 );
839
840 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
841 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
842 }
843 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
844
845 /**
846 * For themes without theme.json file, make sure
847 * to restore the inner div for the group block
848 * to avoid breaking styles relying on that div.
849 *
850 * @param string $block_content Rendered block content.
851 * @param array $block Block object.
852 * @return string Filtered block content.
853 */
854 function gutenberg_restore_group_inner_container( $block_content, $block ) {
855 $tag_name = isset( $block['attrs']['tagName'] ) ? $block['attrs']['tagName'] : 'div';
856 $group_with_inner_container_regex = sprintf(
857 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
858 preg_quote( $tag_name, '/' )
859 );
860 if (
861 wp_theme_has_theme_json() ||
862 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
863 ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) )
864 ) {
865 return $block_content;
866 }
867
868 $replace_regex = sprintf(
869 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
870 preg_quote( $tag_name, '/' )
871 );
872 $updated_content = preg_replace_callback(
873 $replace_regex,
874 static function ( $matches ) {
875 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
876 },
877 $block_content
878 );
879 return $updated_content;
880 }
881
882 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
883 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10, 2 );
884 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10, 2 );
885 }
886 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
887
888
889 /**
890 * For themes without theme.json file, make sure
891 * to restore the outer div for the aligned image block
892 * to avoid breaking styles relying on that div.
893 *
894 * @param string $block_content Rendered block content.
895 * @param array $block Block object.
896 * @return string Filtered block content.
897 */
898 function gutenberg_restore_image_outer_container( $block_content, $block ) {
899 $image_with_align = "
900 /# 1) everything up to the class attribute contents
901 (
902 ^\s*
903 <figure\b
904 [^>]*
905 \bclass=
906 [\"']
907 )
908 # 2) the class attribute contents
909 (
910 [^\"']*
911 \bwp-block-image\b
912 [^\"']*
913 \b(?:alignleft|alignright|aligncenter)\b
914 [^\"']*
915 )
916 # 3) everything after the class attribute contents
917 (
918 [\"']
919 [^>]*
920 >
921 .*
922 <\/figure>
923 )/iUx";
924
925 if (
926 wp_theme_has_theme_json() ||
927 0 === preg_match( $image_with_align, $block_content, $matches )
928 ) {
929 return $block_content;
930 }
931
932 $wrapper_classnames = array( 'wp-block-image' );
933
934 // If the block has a classNames attribute these classnames need to be removed from the content and added back
935 // to the new wrapper div also.
936 if ( ! empty( $block['attrs']['className'] ) ) {
937 $wrapper_classnames = array_merge( $wrapper_classnames, explode( ' ', $block['attrs']['className'] ) );
938 }
939 $content_classnames = explode( ' ', $matches[2] );
940 $filtered_content_classnames = array_diff( $content_classnames, $wrapper_classnames );
941
942 return '<div class="' . implode( ' ', $wrapper_classnames ) . '">' . $matches[1] . implode( ' ', $filtered_content_classnames ) . $matches[3] . '</div>';
943 }
944
945 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
946 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10, 2 );
947 }
948 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
949