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

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

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