PluginProbe
Gutenberg / 14.5.2
Gutenberg v14.5.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 14.5.2, at lib/block-supports/layout.php

568 lines 20.1 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 * Registers the layout block attribute for block types that support it.
10 *
11 * @param WP_Block_Type $block_type Block Type.
12 */
13 function gutenberg_register_layout_support( $block_type ) {
14 $support_layout = block_has_support( $block_type, array( '__experimentalLayout' ), false );
15 if ( $support_layout ) {
16 if ( ! $block_type->attributes ) {
17 $block_type->attributes = array();
18 }
19
20 if ( ! array_key_exists( 'layout', $block_type->attributes ) ) {
21 $block_type->attributes['layout'] = array(
22 'type' => 'object',
23 );
24 }
25 }
26 }
27
28 /**
29 * Generates the CSS corresponding to the provided layout.
30 *
31 * @param string $selector CSS selector.
32 * @param array $layout Layout object. The one that is passed has already checked
33 * the existence of default block layout.
34 * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false.
35 * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null.
36 * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false.
37 * @param string $fallback_gap_value Optional. The block gap value to apply. Default '0.5em'.
38 * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null.
39 * @return string CSS styles on success. Else, empty string.
40 */
41 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 ) {
42 $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default';
43 $layout_styles = array();
44
45 if ( 'default' === $layout_type ) {
46 if ( $has_block_gap_support ) {
47 if ( is_array( $gap_value ) ) {
48 $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
49 }
50 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
51 // Get spacing CSS variable from preset value if provided.
52 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
53 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
54 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
55 $gap_value = "var(--wp--preset--spacing--$slug)";
56 }
57
58 array_push(
59 $layout_styles,
60 array(
61 'selector' => "$selector > *",
62 'declarations' => array(
63 'margin-block-start' => '0',
64 'margin-block-end' => '0',
65 ),
66 ),
67 array(
68 'selector' => "$selector$selector > * + *",
69 'declarations' => array(
70 'margin-block-start' => $gap_value,
71 'margin-block-end' => '0',
72 ),
73 )
74 );
75 }
76 }
77 } elseif ( 'constrained' === $layout_type ) {
78 $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : '';
79 $wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : '';
80 $justify_content = isset( $layout['justifyContent'] ) ? $layout['justifyContent'] : 'center';
81
82 $all_max_width_value = $content_size ? $content_size : $wide_size;
83 $wide_max_width_value = $wide_size ? $wide_size : $content_size;
84
85 // Make sure there is a single CSS rule, and all tags are stripped for security.
86 // TODO: Use `safecss_filter_attr` instead - once https://core.trac.wordpress.org/ticket/46197 is patched.
87 $all_max_width_value = wp_strip_all_tags( explode( ';', $all_max_width_value )[0] );
88 $wide_max_width_value = wp_strip_all_tags( explode( ';', $wide_max_width_value )[0] );
89
90 $margin_left = 'left' === $justify_content ? '0 !important' : 'auto !important';
91 $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
92
93 if ( $content_size || $wide_size ) {
94 array_push(
95 $layout_styles,
96 array(
97 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
98 'declarations' => array(
99 'max-width' => $all_max_width_value,
100 'margin-left' => $margin_left,
101 'margin-right' => $margin_right,
102 ),
103 ),
104 array(
105 'selector' => "$selector > .alignwide",
106 'declarations' => array( 'max-width' => $wide_max_width_value ),
107 ),
108 array(
109 'selector' => "$selector .alignfull",
110 'declarations' => array( 'max-width' => 'none' ),
111 )
112 );
113
114 if ( isset( $block_spacing ) ) {
115 $block_spacing_values = gutenberg_style_engine_get_styles(
116 array(
117 'spacing' => $block_spacing,
118 )
119 );
120
121 /*
122 * Handle negative margins for alignfull children of blocks with custom padding set.
123 * They're added separately because padding might only be set on one side.
124 */
125 if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) {
126 $padding_right = $block_spacing_values['declarations']['padding-right'];
127 $layout_styles[] = array(
128 'selector' => "$selector > .alignfull",
129 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ),
130 );
131 }
132 if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) {
133 $padding_left = $block_spacing_values['declarations']['padding-left'];
134 $layout_styles[] = array(
135 'selector' => "$selector > .alignfull",
136 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ),
137 );
138 }
139 }
140 }
141
142 if ( 'left' === $justify_content ) {
143 $layout_styles[] = array(
144 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
145 'declarations' => array( 'margin-left' => '0 !important' ),
146 );
147 }
148
149 if ( 'right' === $justify_content ) {
150 $layout_styles[] = array(
151 'selector' => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
152 'declarations' => array( 'margin-right' => '0 !important' ),
153 );
154 }
155
156 if ( $has_block_gap_support ) {
157 if ( is_array( $gap_value ) ) {
158 $gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
159 }
160 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
161 // Get spacing CSS variable from preset value if provided.
162 if ( is_string( $gap_value ) && str_contains( $gap_value, 'var:preset|spacing|' ) ) {
163 $index_to_splice = strrpos( $gap_value, '|' ) + 1;
164 $slug = _wp_to_kebab_case( substr( $gap_value, $index_to_splice ) );
165 $gap_value = "var(--wp--preset--spacing--$slug)";
166 }
167
168 array_push(
169 $layout_styles,
170 array(
171 'selector' => "$selector > *",
172 'declarations' => array(
173 'margin-block-start' => '0',
174 'margin-block-end' => '0',
175 ),
176 ),
177 array(
178 'selector' => "$selector$selector > * + *",
179 'declarations' => array(
180 'margin-block-start' => $gap_value,
181 'margin-block-end' => '0',
182 ),
183 )
184 );
185 }
186 }
187 } elseif ( 'flex' === $layout_type ) {
188 $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal';
189
190 $justify_content_options = array(
191 'left' => 'flex-start',
192 'right' => 'flex-end',
193 'center' => 'center',
194 );
195
196 $vertical_alignment_options = array(
197 'top' => 'flex-start',
198 'center' => 'center',
199 'bottom' => 'flex-end',
200 );
201
202 if ( 'horizontal' === $layout_orientation ) {
203 $justify_content_options += array( 'space-between' => 'space-between' );
204 }
205
206 if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
207 $layout_styles[] = array(
208 'selector' => $selector,
209 'declarations' => array( 'flex-wrap' => 'nowrap' ),
210 );
211 }
212
213 if ( $has_block_gap_support && isset( $gap_value ) ) {
214 $combined_gap_value = '';
215 $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
216
217 foreach ( $gap_sides as $gap_side ) {
218 $process_value = is_string( $gap_value ) ? $gap_value : _wp_array_get( $gap_value, array( $gap_side ), $fallback_gap_value );
219 // Get spacing CSS variable from preset value if provided.
220 if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
221 $index_to_splice = strrpos( $process_value, '|' ) + 1;
222 $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
223 $process_value = "var(--wp--preset--spacing--$slug)";
224 }
225 $combined_gap_value .= "$process_value ";
226 }
227 $gap_value = trim( $combined_gap_value );
228
229 if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
230 $layout_styles[] = array(
231 'selector' => $selector,
232 'declarations' => array( 'gap' => $gap_value ),
233 );
234 }
235 }
236
237 if ( 'horizontal' === $layout_orientation ) {
238 /*
239 * Add this style only if is not empty for backwards compatibility,
240 * since we intend to convert blocks that had flex layout implemented
241 * by custom css.
242 */
243 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
244 $layout_styles[] = array(
245 'selector' => $selector,
246 'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ),
247 );
248 }
249
250 if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
251 $layout_styles[] = array(
252 'selector' => $selector,
253 'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
254 );
255 }
256 } else {
257 $layout_styles[] = array(
258 'selector' => $selector,
259 'declarations' => array( 'flex-direction' => 'column' ),
260 );
261 if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
262 $layout_styles[] = array(
263 'selector' => $selector,
264 'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ),
265 );
266 } else {
267 $layout_styles[] = array(
268 'selector' => $selector,
269 'declarations' => array( 'align-items' => 'flex-start' ),
270 );
271 }
272 }
273 }
274
275 if ( ! empty( $layout_styles ) ) {
276 /*
277 * Add to the style engine store to enqueue and render layout styles.
278 * Return compiled layout styles to retain backwards compatibility.
279 * Since https://github.com/WordPress/gutenberg/pull/42452,
280 * wp_enqueue_block_support_styles is no longer called in this block supports file.
281 */
282 return gutenberg_style_engine_get_stylesheet_from_css_rules(
283 $layout_styles,
284 array(
285 'context' => 'block-supports',
286 'prettify' => false,
287 )
288 );
289 }
290
291 return '';
292 }
293
294 /**
295 * Gets classname from last tag in a string of HTML.
296 *
297 * @param string $html markup to be processed.
298 * @return string String of inner wrapper classnames.
299 */
300 function gutenberg_get_classnames_from_last_tag( $html ) {
301 $tags = new WP_HTML_Tag_Processor( $html );
302 $last_classnames = '';
303
304 while ( $tags->next_tag() ) {
305 $last_classnames = $tags->get_attribute( 'class' );
306 }
307
308 return (string) $last_classnames;
309 }
310
311 /**
312 * Renders the layout config to the block wrapper.
313 *
314 * @param string $block_content Rendered block content.
315 * @param array $block Block object.
316 * @return string Filtered block content.
317 */
318 function gutenberg_render_layout_support_flag( $block_content, $block ) {
319 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
320 $support_layout = block_has_support( $block_type, array( '__experimentalLayout' ), false );
321
322 if ( ! $support_layout ) {
323 return $block_content;
324 }
325
326 $block_gap = gutenberg_get_global_settings( array( 'spacing', 'blockGap' ) );
327 $global_layout_settings = gutenberg_get_global_settings( array( 'layout' ) );
328 $has_block_gap_support = isset( $block_gap ) ? null !== $block_gap : false;
329 $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() );
330 $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout;
331
332 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] ) {
333 if ( ! $global_layout_settings ) {
334 return $block_content;
335 }
336 }
337
338 $class_names = array();
339 $layout_definitions = _wp_array_get( $global_layout_settings, array( 'definitions' ), array() );
340 $container_class = wp_unique_id( 'wp-container-' );
341 $layout_classname = '';
342
343 // Set the correct layout type for blocks using legacy content width.
344 if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
345 $used_layout['type'] = 'constrained';
346 }
347
348 if (
349 gutenberg_get_global_settings( array( 'useRootPaddingAwareAlignments' ) ) &&
350 isset( $used_layout['type'] ) &&
351 'constrained' === $used_layout['type']
352 ) {
353 $class_names[] = 'has-global-padding';
354 }
355
356 /*
357 * The following section was added to reintroduce a small set of layout classnames that were
358 * removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
359 * not intended to provide an extended set of classes to match all block layout attributes
360 * here.
361 */
362 if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
363 $class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
364 }
365
366 if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
367 $class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
368 }
369
370 if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
371 $class_names[] = 'is-nowrap';
372 }
373
374 // Get classname for layout type.
375 if ( isset( $used_layout['type'] ) ) {
376 $layout_classname = _wp_array_get( $layout_definitions, array( $used_layout['type'], 'className' ), '' );
377 } else {
378 $layout_classname = _wp_array_get( $layout_definitions, array( 'default', 'className' ), '' );
379 }
380
381 if ( $layout_classname && is_string( $layout_classname ) ) {
382 $class_names[] = sanitize_title( $layout_classname );
383 }
384
385 /*
386 * Only generate Layout styles if the theme has not opted-out.
387 * Attribute-based Layout classnames are output in all cases.
388 */
389 if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
390
391 $gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) );
392
393 /*
394 * Skip if gap value contains unsupported characters.
395 * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
396 * to only match against the value, not the CSS attribute.
397 */
398 if ( is_array( $gap_value ) ) {
399 foreach ( $gap_value as $key => $value ) {
400 $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
401 }
402 } else {
403 $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
404 }
405
406 $fallback_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), '0.5em' );
407 $block_spacing = _wp_array_get( $block, array( 'attrs', 'style', 'spacing' ), null );
408
409 /*
410 * If a block's block.json skips serialization for spacing or spacing.blockGap,
411 * don't apply the user-defined value to the styles.
412 */
413 $should_skip_gap_serialization = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
414
415 $style = gutenberg_get_layout_style(
416 ".$container_class.$container_class",
417 $used_layout,
418 $has_block_gap_support,
419 $gap_value,
420 $should_skip_gap_serialization,
421 $fallback_gap_value,
422 $block_spacing
423 );
424
425 // Only add container class and enqueue block support styles if unique styles were generated.
426 if ( ! empty( $style ) ) {
427 $class_names[] = $container_class;
428 }
429 }
430
431 /**
432 * The first chunk of innerContent contains the block markup up until the inner blocks start.
433 * We want to target the opening tag of the inner blocks wrapper, which is the last tag in that chunk.
434 */
435 $inner_content_classnames = isset( $block['innerContent'][0] ) && 'string' === gettype( $block['innerContent'][0] ) ? gutenberg_get_classnames_from_last_tag( $block['innerContent'][0] ) : '';
436
437 $content = new WP_HTML_Tag_Processor( $block_content );
438 if ( $inner_content_classnames ) {
439 $content->next_tag( array( 'class_name' => $inner_content_classnames ) );
440 foreach ( $class_names as $class_name ) {
441 $content->add_class( $class_name );
442 }
443 } else {
444 $content->next_tag();
445 $content->add_class( implode( ' ', $class_names ) );
446 }
447
448 return (string) $content;
449 }
450
451 // Register the block support. (overrides core one).
452 WP_Block_Supports::get_instance()->register(
453 'layout',
454 array(
455 'register_attribute' => 'gutenberg_register_layout_support',
456 )
457 );
458
459 if ( function_exists( 'wp_render_layout_support_flag' ) ) {
460 remove_filter( 'render_block', 'wp_render_layout_support_flag' );
461 }
462 add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 );
463
464 /**
465 * For themes without theme.json file, make sure
466 * to restore the inner div for the group block
467 * to avoid breaking styles relying on that div.
468 *
469 * @param string $block_content Rendered block content.
470 * @param array $block Block object.
471 * @return string Filtered block content.
472 */
473 function gutenberg_restore_group_inner_container( $block_content, $block ) {
474 $tag_name = isset( $block['attrs']['tagName'] ) ? $block['attrs']['tagName'] : 'div';
475 $group_with_inner_container_regex = sprintf(
476 '/(^\s*<%1$s\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/U',
477 preg_quote( $tag_name, '/' )
478 );
479 if (
480 wp_theme_has_theme_json() ||
481 1 === preg_match( $group_with_inner_container_regex, $block_content ) ||
482 ( isset( $block['attrs']['layout']['type'] ) && 'flex' === $block['attrs']['layout']['type'] )
483 ) {
484 return $block_content;
485 }
486
487 $replace_regex = sprintf(
488 '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms',
489 preg_quote( $tag_name, '/' )
490 );
491 $updated_content = preg_replace_callback(
492 $replace_regex,
493 function( $matches ) {
494 return $matches[1] . '<div class="wp-block-group__inner-container">' . $matches[2] . '</div>' . $matches[3];
495 },
496 $block_content
497 );
498 return $updated_content;
499 }
500
501 if ( function_exists( 'wp_restore_group_inner_container' ) ) {
502 remove_filter( 'render_block', 'wp_restore_group_inner_container', 10, 2 );
503 remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10, 2 );
504 }
505 add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 );
506
507
508 /**
509 * For themes without theme.json file, make sure
510 * to restore the outer div for the aligned image block
511 * to avoid breaking styles relying on that div.
512 *
513 * @param string $block_content Rendered block content.
514 * @param array $block Block object.
515 * @return string Filtered block content.
516 */
517 function gutenberg_restore_image_outer_container( $block_content, $block ) {
518 $image_with_align = "
519 /# 1) everything up to the class attribute contents
520 (
521 ^\s*
522 <figure\b
523 [^>]*
524 \bclass=
525 [\"']
526 )
527 # 2) the class attribute contents
528 (
529 [^\"']*
530 \bwp-block-image\b
531 [^\"']*
532 \b(?:alignleft|alignright|aligncenter)\b
533 [^\"']*
534 )
535 # 3) everything after the class attribute contents
536 (
537 [\"']
538 [^>]*
539 >
540 .*
541 <\/figure>
542 )/iUx";
543
544 if (
545 wp_theme_has_theme_json() ||
546 0 === preg_match( $image_with_align, $block_content, $matches )
547 ) {
548 return $block_content;
549 }
550
551 $wrapper_classnames = array( 'wp-block-image' );
552
553 // If the block has a classNames attribute these classnames need to be removed from the content and added back
554 // to the new wrapper div also.
555 if ( ! empty( $block['attrs']['className'] ) ) {
556 $wrapper_classnames = array_merge( $wrapper_classnames, explode( ' ', $block['attrs']['className'] ) );
557 }
558 $content_classnames = explode( ' ', $matches[2] );
559 $filtered_content_classnames = array_diff( $content_classnames, $wrapper_classnames );
560
561 return '<div class="' . implode( ' ', $wrapper_classnames ) . '">' . $matches[1] . implode( ' ', $filtered_content_classnames ) . $matches[3] . '</div>';
562 }
563
564 if ( function_exists( 'wp_restore_image_outer_container' ) ) {
565 remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10, 2 );
566 }
567 add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 );
568