PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
gutenberg / build / scripts / block-library / gallery.php

gallery.php in Gutenberg 24.1.0, at build/scripts/block-library/gallery.php

805 lines 29.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server-side rendering of the `core/gallery` block.
4 *
5 * @package WordPress
6 */
7
8 /**
9 * Handles backwards compatibility for Gallery Blocks,
10 * whose images feature a `data-id` attribute.
11 *
12 * Now that the Gallery Block contains inner Image Blocks,
13 * we add a custom `data-id` attribute before rendering the gallery
14 * so that the Image Block can pick it up in its render_callback.
15 *
16 * @since 5.9.0
17 *
18 * @param array $parsed_block The block being rendered.
19 * @return array The migrated block object.
20 */
21 function gutenberg_block_core_gallery_data_id_backcompatibility( $parsed_block ) {
22 if ( 'core/gallery' === $parsed_block['blockName'] ) {
23 foreach ( $parsed_block['innerBlocks'] as $key => $inner_block ) {
24 if ( 'core/image' === $inner_block['blockName'] ) {
25 if ( ! isset( $parsed_block['innerBlocks'][ $key ]['attrs']['data-id'] ) && isset( $inner_block['attrs']['id'] ) ) {
26 $parsed_block['innerBlocks'][ $key ]['attrs']['data-id'] = esc_attr( $inner_block['attrs']['id'] );
27 }
28 }
29 }
30 }
31
32 return $parsed_block;
33 }
34
35 add_filter( 'render_block_data', 'gutenberg_block_core_gallery_data_id_backcompatibility' );
36
37 /**
38 * Adds a unique ID to the gallery block context.
39 *
40 * @since 7.0.0
41 *
42 * @param array $context Default context.
43 * @param array $parsed_block Block being rendered, filtered by render_block_data.
44 * @return array Filtered context.
45 */
46 function gutenberg_block_core_gallery_render_context( $context, $parsed_block ) {
47 if ( 'core/gallery' === $parsed_block['blockName'] ) {
48 $context['galleryId'] = uniqid();
49 }
50 return $context;
51 }
52
53 add_filter( 'render_block_context', 'gutenberg_block_core_gallery_render_context', 10, 2 );
54
55 /**
56 * Returns the column gap value used for Gallery image width calculations.
57 *
58 * @since 7.1.0
59 *
60 * @param string|array|null $gap Gallery block gap value.
61 * @param string $fallback_gap Fallback gap value.
62 * @return string Gallery column gap value.
63 */
64 function gutenberg_block_core_gallery_get_column_gap_value( $gap, $fallback_gap ) {
65 if ( is_array( $gap ) ) {
66 $gap = $gap['left'] ?? $fallback_gap;
67 }
68
69 // Make sure $gap is a string to avoid PHP 8.1 deprecation error in preg_match() when the value is null.
70 $gap = is_string( $gap ) ? $gap : '';
71
72 // Skip if gap value contains unsupported characters.
73 // Regex for CSS value borrowed from `safecss_filter_attr`, and used here
74 // because we only want to match against the value, not the CSS attribute.
75 $gap = $gap && preg_match( '%[\\\(&=}]|/\*%', $gap ) ? null : $gap;
76
77 // Get spacing CSS variable from preset value if provided.
78 if ( is_string( $gap ) && str_contains( $gap, 'var:preset|spacing|' ) ) {
79 $index_to_splice = strrpos( $gap, '|' ) + 1;
80 $slug = _wp_to_kebab_case( substr( $gap, $index_to_splice ) );
81 $gap = "var(--wp--preset--spacing--$slug)";
82 }
83
84 $gap_column = ( null !== $gap && '' !== $gap ) ? $gap : $fallback_gap;
85
86 // The unstable gallery gap calculation requires a real value (such as `0px`) and not `0`.
87 return '0' === $gap_column ? '0px' : $gap_column;
88 }
89
90 /**
91 * Returns whether a value can be used as a Gallery aspect ratio.
92 *
93 * Aspect ratios are interpolated into a generated stylesheet instead of being
94 * set as an inline style, so only the numeric forms produced by aspect ratio
95 * presets (and `auto`) are accepted. A value from saved content that isn't one
96 * of those is ignored rather than emitted, so it can't close the rule early and
97 * inject declarations of its own. The editor applies the same restriction in
98 * `isValidGalleryAspectRatio()`.
99 *
100 * @since 7.1.0
101 *
102 * @param mixed $value Value to check.
103 * @return bool Whether the value is a valid aspect ratio.
104 */
105 function gutenberg_block_core_gallery_is_valid_aspect_ratio( $value ) {
106 return is_string( $value ) && 1 === preg_match( '#^(auto|\d+(\.\d+)?(\s*/\s*\d+(\.\d+)?)?)$#', trim( $value ) );
107 }
108
109 /**
110 * Returns Gallery-specific responsive aspect ratio rules for a viewport.
111 *
112 * Unlike the column and crop rules, these are not scoped to the Flex layout:
113 * the aspect ratio applies to the images in every Gallery layout. They also
114 * cover dynamic galleries, whose images are rendered by the Gallery rather than
115 * carrying their own block attributes.
116 *
117 * @since 7.1.0
118 *
119 * @param string $selector Gallery block selector.
120 * @param mixed $viewport_style Viewport style data.
121 * @param string $media_query Viewport media query.
122 * @return array[] Gallery responsive aspect ratio rules.
123 */
124 function gutenberg_block_core_gallery_get_responsive_aspect_ratio_style_rules( $selector, $viewport_style, $media_query ) {
125 if ( ! is_array( $viewport_style ) || ! is_string( $media_query ) ) {
126 return array();
127 }
128
129 $aspect_ratio = $viewport_style['aspectRatio'] ?? null;
130 if ( ! gutenberg_block_core_gallery_is_valid_aspect_ratio( $aspect_ratio ) ) {
131 return array();
132 }
133
134 $aspect_ratio = trim( $aspect_ratio );
135
136 /*
137 * The base aspect ratio is an inline style on each image, so these
138 * declarations have to be important to win for the viewport.
139 *
140 * Original cancels the base ratio, which means rolling the declaration out
141 * of the cascade rather than giving it a value. `auto` - and `initial`,
142 * `unset` and `revert`, which all compute to it - would override the
143 * `width`/`height` presentational hint that gives a lazy-loaded image its
144 * placeholder ratio, collapsing the image to zero height until it loads:
145 * the Featured Image bug fixed in #80386. `revert-layer` drops the
146 * declaration instead, so the image falls back to that hint while loading,
147 * to its natural ratio once loaded, and to any ratio a theme set in a lower
148 * cascade layer. `object-fit` is left as the base set it, so a cropped
149 * Gallery still crops.
150 */
151 $declarations = 'auto' === $aspect_ratio
152 ? array( 'aspect-ratio' => 'revert-layer !important' )
153 : array(
154 'aspect-ratio' => "{$aspect_ratio} !important",
155 'object-fit' => 'cover !important',
156 );
157
158 return array(
159 array(
160 'selector' => "{$selector}.wp-block-gallery.has-nested-images figure.wp-block-image:not(#individual-image) img",
161 'declarations' => $declarations,
162 'rules_group' => $media_query,
163 ),
164 );
165 }
166
167 /**
168 * Returns Gallery-specific responsive Flex rules for a viewport.
169 *
170 * @since 7.1.0
171 *
172 * @param string $selector Gallery block selector.
173 * @param mixed $viewport_style Viewport style data.
174 * @param string $media_query Viewport media query.
175 * @return array[] Gallery responsive Flex rules.
176 */
177 function gutenberg_block_core_gallery_get_responsive_flex_style_rules( $selector, $viewport_style, $media_query ) {
178 if ( ! is_array( $viewport_style ) || ! is_string( $media_query ) ) {
179 return array();
180 }
181
182 $rules = array();
183 $gallery_selector = "{$selector}.wp-block-gallery.has-nested-images:where(.is-layout-flex)";
184 $image_selector = "{$gallery_selector} figure.wp-block-image:not(#individual-image)";
185 $columns = $viewport_style['columns'] ?? null;
186
187 if ( is_int( $columns ) && $columns >= 1 && $columns <= 8 ) {
188 $width = 1 === $columns
189 ? '100%'
190 : sprintf(
191 'calc((100%% - (var(--wp--style--unstable-gallery-gap, 16px) * %1$d)) / %2$d)',
192 $columns - 1,
193 $columns
194 );
195 $rules[] = array(
196 'selector' => $image_selector,
197 'declarations' => array( 'width' => "{$width} !important" ),
198 'rules_group' => $media_query,
199 );
200 }
201
202 $image_crop = $viewport_style['imageCrop'] ?? null;
203 if ( ! is_bool( $image_crop ) ) {
204 return $rules;
205 }
206
207 $rules[] = array(
208 'selector' => $image_selector,
209 'declarations' => $image_crop
210 ? array(
211 'align-self' => 'inherit !important',
212 'margin-bottom' => '0 !important',
213 )
214 : array(
215 'align-self' => 'auto !important',
216 'margin-top' => '0 !important',
217 'margin-bottom' => 'auto !important',
218 ),
219 'rules_group' => $media_query,
220 );
221 $rules[] = array(
222 'selector' => "{$image_selector} > div:not(.components-drop-zone)",
223 'declarations' => array( 'display' => $image_crop ? 'flex !important' : 'block !important' ),
224 'rules_group' => $media_query,
225 );
226 $rules[] = array(
227 'selector' => "{$image_selector} > a",
228 'declarations' => array( 'display' => $image_crop ? 'flex !important' : 'inline-block !important' ),
229 'rules_group' => $media_query,
230 );
231 $rules[] = array(
232 'selector' => "{$image_selector} a,{$image_selector} img",
233 'declarations' => $image_crop
234 ? array(
235 'width' => '100% !important',
236 'flex' => '1 0 0% !important',
237 'height' => '100% !important',
238 'object-fit' => 'cover !important',
239 )
240 : array(
241 'width' => 'auto !important',
242 'flex' => '0 1 auto !important',
243 'height' => 'auto !important',
244 'object-fit' => 'fill !important',
245 ),
246 'rules_group' => $media_query,
247 );
248
249 return $rules;
250 }
251
252 /**
253 * Resolves a Gallery block's `dynamicContent` to an ordered list of image
254 * attachment IDs.
255 *
256 * The `source` key is the dispatch discriminator and `args` holds the source's
257 * parameters. This `{ source, args }` shape mirrors the Block Bindings metadata
258 * shape so dynamic mode can migrate to an `innerBlocks` binding with minimal
259 * change. `core/attached-media` is a context-relative anchor (the post the gallery is
260 * rendered within); future sources translate their REST-named `args` (`author`,
261 * `categories`, `after`/`before`, `media_type`, etc.) into `WP_Query` arguments
262 * here.
263 *
264 * @since 7.0.0
265 *
266 * @param array $source The gallery's `dynamicContent` attribute.
267 * @param WP_Block $block The gallery block instance being rendered.
268 * @return int[] Ordered list of image attachment IDs.
269 */
270 function gutenberg_block_core_gallery_resolve_dynamic_source( $source, $block ) {
271 if ( ! is_array( $source ) ) {
272 return array();
273 }
274
275 $source_name = $source['source'] ?? null;
276 $args = isset( $source['args'] ) && is_array( $source['args'] ) ? $source['args'] : array();
277
278 switch ( $source_name ) {
279 case 'core/attached-media':
280 // Prefer the post supplied via block context, falling back to the post
281 // being rendered. The fallback is what lets a post-bound template (e.g.
282 // `single`/`page`) resolve against the actual post at render time even
283 // though the editor has no concrete post to preview — the editor gates
284 // the dynamic-mode UI on that same context (see `use-dynamic-gallery.js`).
285 $post_id = $block->context['postId'] ?? get_the_ID();
286 if ( ! $post_id ) {
287 return array();
288 }
289
290 // Map the camelCase `args` (block-attribute convention) to WP_Query
291 // names, defaulting to the same order as the editor preview (see
292 // `dynamic-source.js`). Only REST-supported orderby values are
293 // allowed; `menu_order` is intentionally unsupported (it isn't a
294 // valid media REST `orderby`).
295 $orderby = $args['orderBy'] ?? 'date';
296 if ( ! in_array( $orderby, array( 'date', 'title' ), true ) ) {
297 $orderby = 'date';
298 }
299 $order = strtoupper( $args['order'] ?? 'desc' ) === 'ASC' ? 'ASC' : 'DESC';
300
301 // Bound the number of resolved images until the gallery supports
302 // pagination. Kept in sync with the editor query's `per_page` cap; a
303 // case-insensitive grep for `max_images` finds both this and
304 // `MAX_IMAGES` in `dynamic-source.js`.
305 $max_images = 100;
306
307 $query = new WP_Query(
308 array(
309 'post_parent' => $post_id,
310 'post_type' => 'attachment',
311 'post_status' => 'inherit',
312 'post_mime_type' => 'image',
313 'orderby' => $orderby,
314 'order' => $order,
315 'posts_per_page' => $max_images,
316 'fields' => 'ids',
317 'no_found_rows' => true,
318 )
319 );
320
321 return array_map( 'intval', $query->posts );
322 }
323
324 // Unknown or not-yet-implemented source type.
325 return array();
326 }
327
328 /**
329 * Builds the link-related image block attributes for a dynamically rendered
330 * gallery image, mapping the gallery-wide `linkTo` setting onto a single image.
331 *
332 * Mirrors the editor's `getHrefAndDestination()` (see `gallery/utils.js`).
333 *
334 * @since 7.0.0
335 *
336 * @param int $attachment_id The image attachment ID.
337 * @param array $attributes The gallery block attributes.
338 * @return array Partial image block attributes (`href`, `linkDestination`,
339 * `linkTarget`, `rel`, `lightbox`).
340 */
341 function gutenberg_block_core_gallery_dynamic_image_link_attributes( $attachment_id, $attributes ) {
342 $link_to = $attributes['linkTo'] ?? 'none';
343 $attrs = array();
344
345 switch ( $link_to ) {
346 // Gutenberg uses 'media'/'attachment'; WP Core uses 'file'/'post'.
347 case 'media':
348 case 'file':
349 $attrs['href'] = wp_get_attachment_url( $attachment_id );
350 $attrs['linkDestination'] = 'media';
351 break;
352 case 'attachment':
353 case 'post':
354 $attrs['href'] = get_attachment_link( $attachment_id );
355 $attrs['linkDestination'] = 'attachment';
356 break;
357 case 'lightbox':
358 $attrs['linkDestination'] = 'none';
359 $attrs['lightbox'] = array( 'enabled' => true );
360 break;
361 }
362
363 if ( ! empty( $attrs['href'] ) && '_blank' === ( $attributes['linkTarget'] ?? '' ) ) {
364 $attrs['linkTarget'] = '_blank';
365 $attrs['rel'] = 'noopener';
366 }
367
368 return $attrs;
369 }
370
371 /**
372 * Renders a single `core/image` block for a Gallery block running in dynamic
373 * mode, applying the gallery-wide settings that affect how an image renders.
374 *
375 * The image markup is generated here (via `wp_get_attachment_image()`) and
376 * rendered through a real `core/image` block instance so that the image block's
377 * own render callback and lightbox behavior run, and so the gallery's existing
378 * lightbox/interactivity post-processing can pick it up.
379 *
380 * @since 7.0.0
381 *
382 * @param int $attachment_id The image attachment ID.
383 * @param array $attributes The gallery block attributes.
384 * @param array $context Context to expose to the inner image block.
385 * @return string The rendered image block HTML, or an empty string on failure.
386 */
387 function gutenberg_block_core_gallery_render_dynamic_image( $attachment_id, $attributes, $context ) {
388 $size_slug = $attributes['sizeSlug'] ?? 'large';
389 $aspect_ratio = $attributes['aspectRatio'] ?? 'auto';
390
391 $img_attr = array( 'class' => 'wp-image-' . $attachment_id );
392 if ( $aspect_ratio && 'auto' !== $aspect_ratio ) {
393 // Run the aspect ratio through the same sanitization used for every other
394 // block inline style, so an unsafe value can't break out of the style
395 // attribute or inject additional markup.
396 $img_attr['style'] = safecss_filter_attr(
397 sprintf( 'aspect-ratio:%s;object-fit:cover;', $aspect_ratio )
398 );
399 }
400
401 $image_markup = wp_get_attachment_image( $attachment_id, $size_slug, false, $img_attr );
402 if ( ! $image_markup ) {
403 return '';
404 }
405
406 $image_attributes = array_merge(
407 array(
408 'id' => $attachment_id,
409 'data-id' => (string) $attachment_id,
410 'sizeSlug' => $size_slug,
411 ),
412 gutenberg_block_core_gallery_dynamic_image_link_attributes( $attachment_id, $attributes )
413 );
414
415 if ( $aspect_ratio && 'auto' !== $aspect_ratio ) {
416 $image_attributes['aspectRatio'] = $aspect_ratio;
417 $image_attributes['scale'] = 'cover';
418 }
419
420 // Wrap in a link when the gallery links images somewhere.
421 if ( ! empty( $image_attributes['href'] ) ) {
422 $image_markup = sprintf(
423 '<a href="%1$s"%2$s%3$s>%4$s</a>',
424 esc_url( $image_attributes['href'] ),
425 isset( $image_attributes['linkTarget'] ) ? ' target="' . esc_attr( $image_attributes['linkTarget'] ) . '"' : '',
426 isset( $image_attributes['rel'] ) ? ' rel="' . esc_attr( $image_attributes['rel'] ) . '"' : '',
427 $image_markup
428 );
429 }
430
431 // Use the raw caption (`post_excerpt`) so the frontend mirrors the editor
432 // preview, which builds the caption from the REST `caption.raw` value. Gap:
433 // the REST API exposes no caption run through `wp_get_attachment_caption`, so
434 // that filter isn't applied here either.
435 $attachment = get_post( $attachment_id );
436 $caption = $attachment ? $attachment->post_excerpt : '';
437 if ( '' !== $caption ) {
438 $image_markup .= sprintf(
439 '<figcaption class="wp-element-caption">%s</figcaption>',
440 wp_kses_post( $caption )
441 );
442 }
443
444 $figure = sprintf(
445 '<figure class="wp-block-image size-%1$s">%2$s</figure>',
446 esc_attr( $size_slug ),
447 $image_markup
448 );
449
450 $image_block = array(
451 'blockName' => 'core/image',
452 'attrs' => $image_attributes,
453 'innerBlocks' => array(),
454 'innerHTML' => $figure,
455 'innerContent' => array( $figure ),
456 );
457
458 return ( new WP_Block( $image_block, $context ) )->render();
459 }
460
461 /**
462 * Renders the `core/gallery` block on the server.
463 *
464 * @since 6.0.0
465 *
466 * @param array $attributes Attributes of the block being rendered.
467 * @param string $content Content of the block being rendered.
468 * @param array $block The block instance being rendered.
469 * @return string The content of the block being rendered.
470 */
471 function gutenberg_block_core_gallery_render( $attributes, $content, $block ) {
472 static $global_styles = null;
473
474 // Gallery blocks created before layout variations existed do not have an
475 // explicit layout attribute. Missing and malformed layout data therefore
476 // falls back to Flex so existing galleries retain their current appearance.
477 $layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array();
478 $layout_type = $layout['type'] ?? null;
479 $is_flex_layout = ! is_string( $layout_type ) || '' === $layout_type || 'flex' === $layout_type;
480
481 // In dynamic mode the gallery's images are resolved at render time instead of
482 // being authored as inner blocks, so `save.jsx` persists at most the
483 // gallery-level caption — a bare `<figcaption>`, or nothing when there is no
484 // caption. Resolve the configured source to a list of attachments, render an
485 // image block for each, and build the gallery `<figure>` wrapper from scratch.
486 // The gap/randomOrder/lightbox post-processing below then runs over the
487 // constructed markup unchanged.
488 if ( ! empty( $attributes['dynamicContent'] ) ) {
489 $attachment_ids = gutenberg_block_core_gallery_resolve_dynamic_source( $attributes['dynamicContent'], $block );
490
491 // Nothing resolved — no attachments, or an unrecognized source. Render
492 // nothing rather than an empty gallery wrapper; a saved caption is
493 // meaningless without images, so it is intentionally dropped too.
494 if ( empty( $attachment_ids ) ) {
495 return '';
496 }
497
498 // The source query only fetched IDs (`fields => ids`), which skips
499 // WP_Query's cache priming. Each image rendered below reads the
500 // attachment post and its meta (via `wp_get_attachment_image()`,
501 // `get_post()`, etc.), so warm the post and meta caches in a single pair
502 // of queries up front instead of paying ~two queries per attachment.
503 // Term cache is left cold: the render path doesn't read attachment terms.
504 if ( count( $attachment_ids ) > 1 ) {
505 _prime_post_caches( $attachment_ids, false, true );
506 }
507
508 // Expose the gallery's provided context (plus galleryId/postId/postType)
509 // to each image block, since these images are rendered outside the
510 // gallery's real inner-block tree.
511 $image_context = array_merge(
512 is_array( $block->context ) ? $block->context : array(),
513 array(
514 'allowResize' => $attributes['allowResize'] ?? false,
515 'imageCrop' => $attributes['imageCrop'] ?? true,
516 'fixedHeight' => $attributes['fixedHeight'] ?? true,
517 'navigationButtonType' => $attributes['navigationButtonType'] ?? 'icon',
518 )
519 );
520
521 $images_markup = '';
522 foreach ( $attachment_ids as $attachment_id ) {
523 $images_markup .= gutenberg_block_core_gallery_render_dynamic_image( $attachment_id, $attributes, $image_context );
524 }
525
526 // Build the wrapper rather than parsing/splicing saved markup.
527 // `get_block_wrapper_attributes()` supplies the block-support
528 // classes/styles (align, color, border, spacing, anchor id); the layout
529 // render filter adds the active layout classes downstream — the same way a
530 // static gallery's wrapper is composed (`useBlockProps.save()` plus that
531 // filter). Only the gallery-specific classes are added explicitly, and
532 // they mirror `save.jsx` (kept in sync deliberately — see that file).
533 $gallery_classes = 'wp-block-gallery has-nested-images';
534 if ( $is_flex_layout ) {
535 $gallery_classes .= isset( $attributes['columns'] )
536 ? ' columns-' . (int) $attributes['columns']
537 : ' columns-default';
538 if ( $attributes['imageCrop'] ?? true ) {
539 $gallery_classes .= ' is-cropped';
540 }
541 }
542 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $gallery_classes ) );
543
544 // In dynamic mode `save.jsx` persists only the gallery-level caption, so
545 // `$content` is the saved `<figcaption>` (or empty). Append it after the
546 // resolved images — matching the static gallery's `{images}{caption}`
547 // order — without parsing it.
548 $content = sprintf( '<figure %s>%s%s</figure>', $wrapper_attributes, $images_markup, $content );
549 }
550
551 $processed_content = new WP_HTML_Tag_Processor( $content );
552 $processed_content->next_tag();
553
554 $style_attr = is_array( $attributes['style'] ?? null )
555 ? $attributes['style']
556 : array();
557 if (
558 defined( 'IS_GUTENBERG_PLUGIN' ) &&
559 IS_GUTENBERG_PLUGIN &&
560 function_exists( 'gutenberg_resolve_style_state_aliases' )
561 ) {
562 $style_attr = gutenberg_resolve_style_state_aliases( $style_attr, 'core/gallery' );
563 }
564
565 $global_settings = gutenberg_get_global_settings();
566 $viewport_settings = $global_settings['viewport'] ?? null;
567 $responsive_media_queries = array();
568 foreach ( array( 'WP_Theme_JSON_Gutenberg', 'WP_Theme_JSON' ) as $theme_json_class_name ) {
569 if ( method_exists( $theme_json_class_name, 'get_viewport_media_queries' ) ) {
570 $responsive_media_queries = $theme_json_class_name::get_viewport_media_queries( $viewport_settings );
571 break;
572 }
573 }
574
575 // Columns and cropping are Flex-only, but the aspect ratio applies in every
576 // layout, so a Gallery with a viewport aspect ratio needs the per-instance
577 // stylesheet (and the class scoping it) even when it isn't a Flex Gallery.
578 $has_viewport_aspect_ratio = false;
579 foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
580 $viewport_style = $style_attr[ $breakpoint ] ?? null;
581 if (
582 is_array( $viewport_style ) &&
583 gutenberg_block_core_gallery_is_valid_aspect_ratio( $viewport_style['aspectRatio'] ?? null )
584 ) {
585 $has_viewport_aspect_ratio = true;
586 break;
587 }
588 }
589
590 /*
591 * Only generate the gap styles — and, when nothing else needs it, the unique
592 * classname that exists solely to scope them — if the theme has not opted out
593 * of layout styles. The responsive aspect ratio rules are not layout styles,
594 * so they keep rendering either way.
595 */
596 $should_generate_gap_styles = $is_flex_layout && ! current_theme_supports( 'disable-layout-styles' );
597
598 if ( $should_generate_gap_styles || $has_viewport_aspect_ratio ) {
599 $unique_gallery_classname = wp_unique_id( 'wp-block-gallery-' );
600 $processed_content->add_class( $unique_gallery_classname );
601 $gallery_styles = array();
602
603 if ( $should_generate_gap_styles ) {
604 // Add a style tag for the --wp--style--unstable-gallery-gap var. The
605 // Gallery's custom Flex layout recalculates Image block widths based on
606 // the current gap so it can maintain the selected number of columns.
607
608 // --gallery-block--gutter-size is deprecated. --wp--style--gallery-gap-default should be used by themes that want to set a default
609 // gap on the gallery.
610 $fallback_gap = 'var( --wp--style--gallery-gap-default, var( --gallery-block--gutter-size, var( --wp--style--block-gap, 0.5em ) ) )';
611
612 if ( null === $global_styles ) {
613 $global_styles = function_exists( 'gutenberg_get_global_styles' ) ? gutenberg_get_global_styles() : array();
614 }
615
616 $global_gallery_styles = $global_styles['blocks']['core/gallery'] ?? array();
617 $global_gallery_gap = $global_gallery_styles['spacing']['blockGap'] ?? $fallback_gap;
618 $has_block_gap = is_array( $style_attr['spacing'] ?? null ) && array_key_exists( 'blockGap', $style_attr['spacing'] );
619 // Prefer the block's own gap value, then Gallery global styles. Missing
620 // values fall back to the Gallery blockGap default.
621 $block_gap = $has_block_gap
622 ? $style_attr['spacing']['blockGap']
623 : $global_gallery_gap;
624 $gap_column = gutenberg_block_core_gallery_get_column_gap_value( $block_gap, $fallback_gap );
625
626 // Set the CSS variable to the column value for Gallery's flex width calculations.
627 $gallery_styles[] = array(
628 'selector' => ".wp-block-gallery.{$unique_gallery_classname}",
629 'declarations' => array(
630 '--wp--style--unstable-gallery-gap' => $gap_column,
631 ),
632 );
633 }
634
635 foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
636 $viewport_style = $style_attr[ $breakpoint ] ?? null;
637
638 if ( $should_generate_gap_styles ) {
639 $has_viewport_block_gap = is_array( $viewport_style ) &&
640 is_array( $viewport_style['spacing'] ?? null ) &&
641 array_key_exists( 'blockGap', $viewport_style['spacing'] );
642 $has_global_viewport_block_gap = is_array( $global_gallery_styles[ $breakpoint ]['spacing'] ?? null ) &&
643 array_key_exists( 'blockGap', $global_gallery_styles[ $breakpoint ]['spacing'] );
644
645 // Viewport-specific block values win. Gallery global viewport values
646 // only apply when the block has no base gap, so they do not override an instance value.
647 if ( $has_viewport_block_gap ) {
648 $viewport_gap = $viewport_style['spacing']['blockGap'];
649 } elseif ( ! $has_block_gap && $has_global_viewport_block_gap ) {
650 $viewport_gap = $global_gallery_styles[ $breakpoint ]['spacing']['blockGap'];
651 } else {
652 $viewport_gap = null;
653 }
654
655 if ( null !== $viewport_gap ) {
656 $gallery_styles[] = array(
657 'selector' => ".wp-block-gallery.{$unique_gallery_classname}",
658 'declarations' => array(
659 '--wp--style--unstable-gallery-gap' => gutenberg_block_core_gallery_get_column_gap_value(
660 $viewport_gap,
661 $fallback_gap
662 ),
663 ),
664 'rules_group' => $media_query,
665 );
666 }
667
668 $gallery_styles = array_merge(
669 $gallery_styles,
670 gutenberg_block_core_gallery_get_responsive_flex_style_rules(
671 ".{$unique_gallery_classname}",
672 $viewport_style,
673 $media_query
674 )
675 );
676 }
677
678 $gallery_styles = array_merge(
679 $gallery_styles,
680 gutenberg_block_core_gallery_get_responsive_aspect_ratio_style_rules(
681 ".{$unique_gallery_classname}",
682 $viewport_style,
683 $media_query
684 )
685 );
686 }
687
688 gutenberg_style_engine_get_stylesheet_from_css_rules(
689 $gallery_styles,
690 array( 'context' => 'block-supports' )
691 );
692 }
693
694 // The WP_HTML_Tag_Processor class calls get_updated_html() internally
695 // when the instance is treated as a string, but here we explicitly
696 // convert it to a string.
697 $updated_content = $processed_content->get_updated_html();
698
699 /*
700 * Randomize the order of image blocks. Ideally we should shuffle
701 * the `$parsed_block['innerBlocks']` via the `render_block_data` hook.
702 * However, this hook doesn't apply inner block updates when blocks are
703 * nested.
704 * @todo In the future, if this hook supports updating innerBlocks in
705 * nested blocks, it should be refactored.
706 *
707 * @see: https://github.com/WordPress/gutenberg/pull/58733
708 */
709 if ( ! empty( $attributes['randomOrder'] ) ) {
710 // This pattern matches figure elements with the `wp-block-image`
711 // class to avoid the gallery's wrapping `figure` element and
712 // extract images only.
713 $pattern = '/<figure[^>]*\bwp-block-image\b[^>]*>.*?<\/figure>/s';
714
715 preg_match_all( $pattern, $updated_content, $matches );
716 if ( $matches ) {
717 $image_blocks = $matches[0];
718 shuffle( $image_blocks );
719
720 $i = 0;
721 $updated_content = preg_replace_callback(
722 $pattern,
723 static function () use ( $image_blocks, &$i ) {
724 return $image_blocks[ $i++ ];
725 },
726 $updated_content
727 );
728 }
729 }
730
731 // Gets all image IDs from the state that match this gallery's ID.
732 $state = wp_interactivity_state( 'core/image' );
733 $gallery_id = $block->context['galleryId'] ?? null;
734 $image_ids = array();
735
736 // Extracts image IDs from state metadata that match the current gallery ID.
737 if ( isset( $gallery_id ) && isset( $state['metadata'] ) ) {
738 foreach ( $state['metadata'] as $image_id => $metadata ) {
739 if ( isset( $metadata['galleryId'] ) && $metadata['galleryId'] === $gallery_id ) {
740 $image_ids[] = $image_id;
741 }
742 }
743 }
744
745 // If there are image IDs associated with this gallery, set interactivity
746 // attributes and order metadata for lightbox navigation.
747 if ( ! empty( $image_ids ) ) {
748 $total = count( $image_ids );
749 $lightbox_index = 0;
750 $processor = new WP_HTML_Tag_Processor( $updated_content );
751 $processor->next_tag();
752 $processor->set_attribute( 'data-wp-interactive', 'core/gallery' );
753 $processor->set_attribute(
754 'data-wp-context',
755 wp_json_encode(
756 array( 'galleryId' => $gallery_id ),
757 JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
758 )
759 );
760 while ( $processor->next_tag( 'figure' ) ) {
761 $wp_key = $processor->get_attribute( 'data-wp-key' );
762 if ( $wp_key && isset( $state['metadata'][ $wp_key ] ) ) {
763 $alt = $state['metadata'][ $wp_key ]['alt'];
764 wp_interactivity_state(
765 'core/image',
766 array(
767 'metadata' => array(
768 $wp_key => array(
769 'customAriaLabel' => empty( $alt )
770 /* translators: %1$s: current image index, %2$s: total number of images */
771 ? sprintf( __( 'Enlarged image %1$s of %2$s' ), $lightbox_index + 1, $total )
772 /* translators: %1$s: current image index, %2$s: total number of images, %3$s: Image alt text */
773 : sprintf( __( 'Enlarged image %1$s of %2$s: %3$s' ), $lightbox_index + 1, $total, $alt ),
774 /* translators: %1$s: current image index, %2$s: total number of images */
775 'triggerButtonAriaLabel' => sprintf( __( 'Enlarge %1$s of %2$s' ), $lightbox_index + 1, $total ),
776 'order' => $lightbox_index,
777 ),
778 ),
779 )
780 );
781 ++$lightbox_index;
782 }
783 }
784 return $processor->get_updated_html();
785 }
786
787 return $updated_content;
788 }
789
790 /**
791 * Registers the `core/gallery` block on server.
792 *
793 * @since 5.9.0
794 */
795 function gutenberg_register_block_core_gallery() {
796 register_block_type_from_metadata(
797 __DIR__ . '/gallery',
798 array(
799 'render_callback' => 'gutenberg_block_core_gallery_render',
800 )
801 );
802 }
803
804 add_action( 'init', 'gutenberg_register_block_core_gallery', 20 );
805