PluginProbe
Gutenberg / 24.0.0
Gutenberg v24.0.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 12.6.0 All 403 releases
gutenberg / build / scripts / block-library / gallery.php

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

691 lines 25.4 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 Gallery-specific responsive Flex rules for a viewport.
92 *
93 * @since 7.1.0
94 *
95 * @param string $selector Gallery block selector.
96 * @param mixed $viewport_style Viewport style data.
97 * @param string $media_query Viewport media query.
98 * @return array[] Gallery responsive Flex rules.
99 */
100 function gutenberg_block_core_gallery_get_responsive_flex_style_rules( $selector, $viewport_style, $media_query ) {
101 if ( ! is_array( $viewport_style ) || ! is_string( $media_query ) ) {
102 return array();
103 }
104
105 $rules = array();
106 $gallery_selector = "{$selector}.wp-block-gallery.has-nested-images:where(.is-layout-flex)";
107 $image_selector = "{$gallery_selector} figure.wp-block-image:not(#individual-image)";
108 $columns = $viewport_style['columns'] ?? null;
109
110 if ( is_int( $columns ) && $columns >= 1 && $columns <= 8 ) {
111 $width = 1 === $columns
112 ? '100%'
113 : sprintf(
114 'calc((100%% - (var(--wp--style--unstable-gallery-gap, 16px) * %1$d)) / %2$d)',
115 $columns - 1,
116 $columns
117 );
118 $rules[] = array(
119 'selector' => $image_selector,
120 'declarations' => array( 'width' => "{$width} !important" ),
121 'rules_group' => $media_query,
122 );
123 }
124
125 $image_crop = $viewport_style['imageCrop'] ?? null;
126 if ( ! is_bool( $image_crop ) ) {
127 return $rules;
128 }
129
130 $rules[] = array(
131 'selector' => $image_selector,
132 'declarations' => $image_crop
133 ? array(
134 'align-self' => 'inherit !important',
135 'margin-bottom' => '0 !important',
136 )
137 : array(
138 'align-self' => 'auto !important',
139 'margin-top' => '0 !important',
140 'margin-bottom' => 'auto !important',
141 ),
142 'rules_group' => $media_query,
143 );
144 $rules[] = array(
145 'selector' => "{$image_selector} > div:not(.components-drop-zone)",
146 'declarations' => array( 'display' => $image_crop ? 'flex !important' : 'block !important' ),
147 'rules_group' => $media_query,
148 );
149 $rules[] = array(
150 'selector' => "{$image_selector} > a",
151 'declarations' => array( 'display' => $image_crop ? 'flex !important' : 'inline-block !important' ),
152 'rules_group' => $media_query,
153 );
154 $rules[] = array(
155 'selector' => "{$image_selector} a,{$image_selector} img",
156 'declarations' => $image_crop
157 ? array(
158 'width' => '100% !important',
159 'flex' => '1 0 0% !important',
160 'height' => '100% !important',
161 'object-fit' => 'cover !important',
162 )
163 : array(
164 'width' => 'auto !important',
165 'flex' => '0 1 auto !important',
166 'height' => 'auto !important',
167 'object-fit' => 'fill !important',
168 ),
169 'rules_group' => $media_query,
170 );
171
172 return $rules;
173 }
174
175 /**
176 * Resolves a Gallery block's `dynamicContent` to an ordered list of image
177 * attachment IDs.
178 *
179 * The `source` key is the dispatch discriminator and `args` holds the source's
180 * parameters. This `{ source, args }` shape mirrors the Block Bindings metadata
181 * shape so dynamic mode can migrate to an `innerBlocks` binding with minimal
182 * change. `core/attached-media` is a context-relative anchor (the post the gallery is
183 * rendered within); future sources translate their REST-named `args` (`author`,
184 * `categories`, `after`/`before`, `media_type`, etc.) into `WP_Query` arguments
185 * here.
186 *
187 * @since 7.0.0
188 *
189 * @param array $source The gallery's `dynamicContent` attribute.
190 * @param WP_Block $block The gallery block instance being rendered.
191 * @return int[] Ordered list of image attachment IDs.
192 */
193 function gutenberg_block_core_gallery_resolve_dynamic_source( $source, $block ) {
194 if ( ! is_array( $source ) ) {
195 return array();
196 }
197
198 $source_name = $source['source'] ?? null;
199 $args = isset( $source['args'] ) && is_array( $source['args'] ) ? $source['args'] : array();
200
201 switch ( $source_name ) {
202 case 'core/attached-media':
203 // Prefer the post supplied via block context, falling back to the post
204 // being rendered. The fallback is what lets a post-bound template (e.g.
205 // `single`/`page`) resolve against the actual post at render time even
206 // though the editor has no concrete post to preview — the editor gates
207 // the dynamic-mode UI on that same context (see `use-dynamic-gallery.js`).
208 $post_id = $block->context['postId'] ?? get_the_ID();
209 if ( ! $post_id ) {
210 return array();
211 }
212
213 // Map the camelCase `args` (block-attribute convention) to WP_Query
214 // names, defaulting to the same order as the editor preview (see
215 // `dynamic-source.js`). Only REST-supported orderby values are
216 // allowed; `menu_order` is intentionally unsupported (it isn't a
217 // valid media REST `orderby`).
218 $orderby = $args['orderBy'] ?? 'date';
219 if ( ! in_array( $orderby, array( 'date', 'title' ), true ) ) {
220 $orderby = 'date';
221 }
222 $order = strtoupper( $args['order'] ?? 'desc' ) === 'ASC' ? 'ASC' : 'DESC';
223
224 // Bound the number of resolved images until the gallery supports
225 // pagination. Kept in sync with the editor query's `per_page` cap; a
226 // case-insensitive grep for `max_images` finds both this and
227 // `MAX_IMAGES` in `dynamic-source.js`.
228 $max_images = 100;
229
230 $query = new WP_Query(
231 array(
232 'post_parent' => $post_id,
233 'post_type' => 'attachment',
234 'post_status' => 'inherit',
235 'post_mime_type' => 'image',
236 'orderby' => $orderby,
237 'order' => $order,
238 'posts_per_page' => $max_images,
239 'fields' => 'ids',
240 'no_found_rows' => true,
241 )
242 );
243
244 return array_map( 'intval', $query->posts );
245 }
246
247 // Unknown or not-yet-implemented source type.
248 return array();
249 }
250
251 /**
252 * Builds the link-related image block attributes for a dynamically rendered
253 * gallery image, mapping the gallery-wide `linkTo` setting onto a single image.
254 *
255 * Mirrors the editor's `getHrefAndDestination()` (see `gallery/utils.js`).
256 *
257 * @since 7.0.0
258 *
259 * @param int $attachment_id The image attachment ID.
260 * @param array $attributes The gallery block attributes.
261 * @return array Partial image block attributes (`href`, `linkDestination`,
262 * `linkTarget`, `rel`, `lightbox`).
263 */
264 function gutenberg_block_core_gallery_dynamic_image_link_attributes( $attachment_id, $attributes ) {
265 $link_to = $attributes['linkTo'] ?? 'none';
266 $attrs = array();
267
268 switch ( $link_to ) {
269 // Gutenberg uses 'media'/'attachment'; WP Core uses 'file'/'post'.
270 case 'media':
271 case 'file':
272 $attrs['href'] = wp_get_attachment_url( $attachment_id );
273 $attrs['linkDestination'] = 'media';
274 break;
275 case 'attachment':
276 case 'post':
277 $attrs['href'] = get_attachment_link( $attachment_id );
278 $attrs['linkDestination'] = 'attachment';
279 break;
280 case 'lightbox':
281 $attrs['linkDestination'] = 'none';
282 $attrs['lightbox'] = array( 'enabled' => true );
283 break;
284 }
285
286 if ( ! empty( $attrs['href'] ) && '_blank' === ( $attributes['linkTarget'] ?? '' ) ) {
287 $attrs['linkTarget'] = '_blank';
288 $attrs['rel'] = 'noopener';
289 }
290
291 return $attrs;
292 }
293
294 /**
295 * Renders a single `core/image` block for a Gallery block running in dynamic
296 * mode, applying the gallery-wide settings that affect how an image renders.
297 *
298 * The image markup is generated here (via `wp_get_attachment_image()`) and
299 * rendered through a real `core/image` block instance so that the image block's
300 * own render callback and lightbox behavior run, and so the gallery's existing
301 * lightbox/interactivity post-processing can pick it up.
302 *
303 * @since 7.0.0
304 *
305 * @param int $attachment_id The image attachment ID.
306 * @param array $attributes The gallery block attributes.
307 * @param array $context Context to expose to the inner image block.
308 * @return string The rendered image block HTML, or an empty string on failure.
309 */
310 function gutenberg_block_core_gallery_render_dynamic_image( $attachment_id, $attributes, $context ) {
311 $size_slug = $attributes['sizeSlug'] ?? 'large';
312 $aspect_ratio = $attributes['aspectRatio'] ?? 'auto';
313
314 $img_attr = array( 'class' => 'wp-image-' . $attachment_id );
315 if ( $aspect_ratio && 'auto' !== $aspect_ratio ) {
316 // Run the aspect ratio through the same sanitization used for every other
317 // block inline style, so an unsafe value can't break out of the style
318 // attribute or inject additional markup.
319 $img_attr['style'] = safecss_filter_attr(
320 sprintf( 'aspect-ratio:%s;object-fit:cover;', $aspect_ratio )
321 );
322 }
323
324 $image_markup = wp_get_attachment_image( $attachment_id, $size_slug, false, $img_attr );
325 if ( ! $image_markup ) {
326 return '';
327 }
328
329 $image_attributes = array_merge(
330 array(
331 'id' => $attachment_id,
332 'data-id' => (string) $attachment_id,
333 'sizeSlug' => $size_slug,
334 ),
335 gutenberg_block_core_gallery_dynamic_image_link_attributes( $attachment_id, $attributes )
336 );
337
338 if ( $aspect_ratio && 'auto' !== $aspect_ratio ) {
339 $image_attributes['aspectRatio'] = $aspect_ratio;
340 $image_attributes['scale'] = 'cover';
341 }
342
343 // Wrap in a link when the gallery links images somewhere.
344 if ( ! empty( $image_attributes['href'] ) ) {
345 $image_markup = sprintf(
346 '<a href="%1$s"%2$s%3$s>%4$s</a>',
347 esc_url( $image_attributes['href'] ),
348 isset( $image_attributes['linkTarget'] ) ? ' target="' . esc_attr( $image_attributes['linkTarget'] ) . '"' : '',
349 isset( $image_attributes['rel'] ) ? ' rel="' . esc_attr( $image_attributes['rel'] ) . '"' : '',
350 $image_markup
351 );
352 }
353
354 // Use the raw caption (`post_excerpt`) so the frontend mirrors the editor
355 // preview, which builds the caption from the REST `caption.raw` value. Gap:
356 // the REST API exposes no caption run through `wp_get_attachment_caption`, so
357 // that filter isn't applied here either.
358 $attachment = get_post( $attachment_id );
359 $caption = $attachment ? $attachment->post_excerpt : '';
360 if ( '' !== $caption ) {
361 $image_markup .= sprintf(
362 '<figcaption class="wp-element-caption">%s</figcaption>',
363 wp_kses_post( $caption )
364 );
365 }
366
367 $figure = sprintf(
368 '<figure class="wp-block-image size-%1$s">%2$s</figure>',
369 esc_attr( $size_slug ),
370 $image_markup
371 );
372
373 $image_block = array(
374 'blockName' => 'core/image',
375 'attrs' => $image_attributes,
376 'innerBlocks' => array(),
377 'innerHTML' => $figure,
378 'innerContent' => array( $figure ),
379 );
380
381 return ( new WP_Block( $image_block, $context ) )->render();
382 }
383
384 /**
385 * Renders the `core/gallery` block on the server.
386 *
387 * @since 6.0.0
388 *
389 * @param array $attributes Attributes of the block being rendered.
390 * @param string $content Content of the block being rendered.
391 * @param array $block The block instance being rendered.
392 * @return string The content of the block being rendered.
393 */
394 function gutenberg_block_core_gallery_render( $attributes, $content, $block ) {
395 static $global_styles = null;
396
397 // Gallery blocks created before layout variations existed do not have an
398 // explicit layout attribute. Missing and malformed layout data therefore
399 // falls back to Flex so existing galleries retain their current appearance.
400 $layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array();
401 $layout_type = $layout['type'] ?? null;
402 $is_flex_layout = ! is_string( $layout_type ) || '' === $layout_type || 'flex' === $layout_type;
403
404 // In dynamic mode the gallery's images are resolved at render time instead of
405 // being authored as inner blocks, so `save.jsx` persists at most the
406 // gallery-level caption — a bare `<figcaption>`, or nothing when there is no
407 // caption. Resolve the configured source to a list of attachments, render an
408 // image block for each, and build the gallery `<figure>` wrapper from scratch.
409 // The gap/randomOrder/lightbox post-processing below then runs over the
410 // constructed markup unchanged.
411 if ( ! empty( $attributes['dynamicContent'] ) ) {
412 $attachment_ids = gutenberg_block_core_gallery_resolve_dynamic_source( $attributes['dynamicContent'], $block );
413
414 // Nothing resolved — no attachments, or an unrecognized source. Render
415 // nothing rather than an empty gallery wrapper; a saved caption is
416 // meaningless without images, so it is intentionally dropped too.
417 if ( empty( $attachment_ids ) ) {
418 return '';
419 }
420
421 // The source query only fetched IDs (`fields => ids`), which skips
422 // WP_Query's cache priming. Each image rendered below reads the
423 // attachment post and its meta (via `wp_get_attachment_image()`,
424 // `get_post()`, etc.), so warm the post and meta caches in a single pair
425 // of queries up front instead of paying ~two queries per attachment.
426 // Term cache is left cold: the render path doesn't read attachment terms.
427 if ( count( $attachment_ids ) > 1 ) {
428 _prime_post_caches( $attachment_ids, false, true );
429 }
430
431 // Expose the gallery's provided context (plus galleryId/postId/postType)
432 // to each image block, since these images are rendered outside the
433 // gallery's real inner-block tree.
434 $image_context = array_merge(
435 is_array( $block->context ) ? $block->context : array(),
436 array(
437 'allowResize' => $attributes['allowResize'] ?? false,
438 'imageCrop' => $attributes['imageCrop'] ?? true,
439 'fixedHeight' => $attributes['fixedHeight'] ?? true,
440 'navigationButtonType' => $attributes['navigationButtonType'] ?? 'icon',
441 )
442 );
443
444 $images_markup = '';
445 foreach ( $attachment_ids as $attachment_id ) {
446 $images_markup .= gutenberg_block_core_gallery_render_dynamic_image( $attachment_id, $attributes, $image_context );
447 }
448
449 // Build the wrapper rather than parsing/splicing saved markup.
450 // `get_block_wrapper_attributes()` supplies the block-support
451 // classes/styles (align, color, border, spacing, anchor id); the layout
452 // render filter adds the active layout classes downstream — the same way a
453 // static gallery's wrapper is composed (`useBlockProps.save()` plus that
454 // filter). Only the gallery-specific classes are added explicitly, and
455 // they mirror `save.jsx` (kept in sync deliberately — see that file).
456 $gallery_classes = 'wp-block-gallery has-nested-images';
457 if ( $is_flex_layout ) {
458 $gallery_classes .= isset( $attributes['columns'] )
459 ? ' columns-' . (int) $attributes['columns']
460 : ' columns-default';
461 if ( $attributes['imageCrop'] ?? true ) {
462 $gallery_classes .= ' is-cropped';
463 }
464 }
465 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $gallery_classes ) );
466
467 // In dynamic mode `save.jsx` persists only the gallery-level caption, so
468 // `$content` is the saved `<figcaption>` (or empty). Append it after the
469 // resolved images — matching the static gallery's `{images}{caption}`
470 // order — without parsing it.
471 $content = sprintf( '<figure %s>%s%s</figure>', $wrapper_attributes, $images_markup, $content );
472 }
473
474 $processed_content = new WP_HTML_Tag_Processor( $content );
475 $processed_content->next_tag();
476
477 if ( $is_flex_layout ) {
478 // Add a style tag for the --wp--style--unstable-gallery-gap var. The
479 // Gallery's custom Flex layout recalculates Image block widths based on
480 // the current gap so it can maintain the selected number of columns.
481 $style_attr = is_array( $attributes['style'] ?? null )
482 ? $attributes['style']
483 : array();
484 if (
485 defined( 'IS_GUTENBERG_PLUGIN' ) &&
486 IS_GUTENBERG_PLUGIN &&
487 function_exists( 'gutenberg_resolve_style_state_aliases' )
488 ) {
489 $style_attr = gutenberg_resolve_style_state_aliases( $style_attr, 'core/gallery' );
490 }
491
492 $unique_gallery_classname = wp_unique_id( 'wp-block-gallery-' );
493 $processed_content->add_class( $unique_gallery_classname );
494
495 // --gallery-block--gutter-size is deprecated. --wp--style--gallery-gap-default should be used by themes that want to set a default
496 // gap on the gallery.
497 $fallback_gap = 'var( --wp--style--gallery-gap-default, var( --gallery-block--gutter-size, var( --wp--style--block-gap, 0.5em ) ) )';
498
499 if ( null === $global_styles ) {
500 $global_styles = function_exists( 'gutenberg_get_global_styles' ) ? gutenberg_get_global_styles() : array();
501 }
502
503 $global_gallery_styles = $global_styles['blocks']['core/gallery'] ?? array();
504 $global_gallery_gap = $global_gallery_styles['spacing']['blockGap'] ?? $fallback_gap;
505 $has_block_gap = is_array( $style_attr['spacing'] ?? null ) && array_key_exists( 'blockGap', $style_attr['spacing'] );
506 // Prefer the block's own gap value, then Gallery global styles. Missing
507 // values fall back to the Gallery blockGap default.
508 $block_gap = $has_block_gap
509 ? $style_attr['spacing']['blockGap']
510 : $global_gallery_gap;
511 $gap_column = gutenberg_block_core_gallery_get_column_gap_value( $block_gap, $fallback_gap );
512
513 // Set the CSS variable to the column value for Gallery's flex width calculations.
514 $gallery_styles = array(
515 array(
516 'selector' => ".wp-block-gallery.{$unique_gallery_classname}",
517 'declarations' => array(
518 '--wp--style--unstable-gallery-gap' => $gap_column,
519 ),
520 ),
521 );
522
523 $global_settings = gutenberg_get_global_settings();
524 $viewport_settings = $global_settings['viewport'] ?? null;
525 $responsive_media_queries = array();
526 foreach ( array( 'WP_Theme_JSON_Gutenberg', 'WP_Theme_JSON' ) as $theme_json_class_name ) {
527 if ( method_exists( $theme_json_class_name, 'get_viewport_media_queries' ) ) {
528 $responsive_media_queries = $theme_json_class_name::get_viewport_media_queries( $viewport_settings );
529 break;
530 }
531 }
532
533 foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
534 $viewport_style = $style_attr[ $breakpoint ] ?? null;
535 $has_viewport_block_gap = is_array( $viewport_style ) &&
536 is_array( $viewport_style['spacing'] ?? null ) &&
537 array_key_exists( 'blockGap', $viewport_style['spacing'] );
538 $has_global_viewport_block_gap = is_array( $global_gallery_styles[ $breakpoint ]['spacing'] ?? null ) &&
539 array_key_exists( 'blockGap', $global_gallery_styles[ $breakpoint ]['spacing'] );
540
541 // Viewport-specific block values win. Gallery global viewport values
542 // only apply when the block has no base gap, so they do not override an instance value.
543 if ( $has_viewport_block_gap ) {
544 $viewport_gap = $viewport_style['spacing']['blockGap'];
545 } elseif ( ! $has_block_gap && $has_global_viewport_block_gap ) {
546 $viewport_gap = $global_gallery_styles[ $breakpoint ]['spacing']['blockGap'];
547 } else {
548 $viewport_gap = null;
549 }
550
551 if ( null !== $viewport_gap ) {
552 $gallery_styles[] = array(
553 'selector' => ".wp-block-gallery.{$unique_gallery_classname}",
554 'declarations' => array(
555 '--wp--style--unstable-gallery-gap' => gutenberg_block_core_gallery_get_column_gap_value(
556 $viewport_gap,
557 $fallback_gap
558 ),
559 ),
560 'rules_group' => $media_query,
561 );
562 }
563
564 $gallery_styles = array_merge(
565 $gallery_styles,
566 gutenberg_block_core_gallery_get_responsive_flex_style_rules(
567 ".{$unique_gallery_classname}",
568 $viewport_style,
569 $media_query
570 )
571 );
572 }
573
574 gutenberg_style_engine_get_stylesheet_from_css_rules(
575 $gallery_styles,
576 array( 'context' => 'block-supports' )
577 );
578 }
579
580 // The WP_HTML_Tag_Processor class calls get_updated_html() internally
581 // when the instance is treated as a string, but here we explicitly
582 // convert it to a string.
583 $updated_content = $processed_content->get_updated_html();
584
585 /*
586 * Randomize the order of image blocks. Ideally we should shuffle
587 * the `$parsed_block['innerBlocks']` via the `render_block_data` hook.
588 * However, this hook doesn't apply inner block updates when blocks are
589 * nested.
590 * @todo In the future, if this hook supports updating innerBlocks in
591 * nested blocks, it should be refactored.
592 *
593 * @see: https://github.com/WordPress/gutenberg/pull/58733
594 */
595 if ( ! empty( $attributes['randomOrder'] ) ) {
596 // This pattern matches figure elements with the `wp-block-image`
597 // class to avoid the gallery's wrapping `figure` element and
598 // extract images only.
599 $pattern = '/<figure[^>]*\bwp-block-image\b[^>]*>.*?<\/figure>/s';
600
601 preg_match_all( $pattern, $updated_content, $matches );
602 if ( $matches ) {
603 $image_blocks = $matches[0];
604 shuffle( $image_blocks );
605
606 $i = 0;
607 $updated_content = preg_replace_callback(
608 $pattern,
609 static function () use ( $image_blocks, &$i ) {
610 return $image_blocks[ $i++ ];
611 },
612 $updated_content
613 );
614 }
615 }
616
617 // Gets all image IDs from the state that match this gallery's ID.
618 $state = wp_interactivity_state( 'core/image' );
619 $gallery_id = $block->context['galleryId'] ?? null;
620 $image_ids = array();
621
622 // Extracts image IDs from state metadata that match the current gallery ID.
623 if ( isset( $gallery_id ) && isset( $state['metadata'] ) ) {
624 foreach ( $state['metadata'] as $image_id => $metadata ) {
625 if ( isset( $metadata['galleryId'] ) && $metadata['galleryId'] === $gallery_id ) {
626 $image_ids[] = $image_id;
627 }
628 }
629 }
630
631 // If there are image IDs associated with this gallery, set interactivity
632 // attributes and order metadata for lightbox navigation.
633 if ( ! empty( $image_ids ) ) {
634 $total = count( $image_ids );
635 $lightbox_index = 0;
636 $processor = new WP_HTML_Tag_Processor( $updated_content );
637 $processor->next_tag();
638 $processor->set_attribute( 'data-wp-interactive', 'core/gallery' );
639 $processor->set_attribute(
640 'data-wp-context',
641 wp_json_encode(
642 array( 'galleryId' => $gallery_id ),
643 JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
644 )
645 );
646 while ( $processor->next_tag( 'figure' ) ) {
647 $wp_key = $processor->get_attribute( 'data-wp-key' );
648 if ( $wp_key && isset( $state['metadata'][ $wp_key ] ) ) {
649 $alt = $state['metadata'][ $wp_key ]['alt'];
650 wp_interactivity_state(
651 'core/image',
652 array(
653 'metadata' => array(
654 $wp_key => array(
655 'customAriaLabel' => empty( $alt )
656 /* translators: %1$s: current image index, %2$s: total number of images */
657 ? sprintf( __( 'Enlarged image %1$s of %2$s' ), $lightbox_index + 1, $total )
658 /* translators: %1$s: current image index, %2$s: total number of images, %3$s: Image alt text */
659 : sprintf( __( 'Enlarged image %1$s of %2$s: %3$s' ), $lightbox_index + 1, $total, $alt ),
660 /* translators: %1$s: current image index, %2$s: total number of images */
661 'triggerButtonAriaLabel' => sprintf( __( 'Enlarge %1$s of %2$s' ), $lightbox_index + 1, $total ),
662 'order' => $lightbox_index,
663 ),
664 ),
665 )
666 );
667 ++$lightbox_index;
668 }
669 }
670 return $processor->get_updated_html();
671 }
672
673 return $updated_content;
674 }
675
676 /**
677 * Registers the `core/gallery` block on server.
678 *
679 * @since 5.9.0
680 */
681 function gutenberg_register_block_core_gallery() {
682 register_block_type_from_metadata(
683 __DIR__ . '/gallery',
684 array(
685 'render_callback' => 'gutenberg_block_core_gallery_render',
686 )
687 );
688 }
689
690 add_action( 'init', 'gutenberg_register_block_core_gallery', 20 );
691