PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
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
← All changes | build/scripts/block-library/gallery.php +611 -58 23.5.2 → trunk View file →
@@ -52,8 +52,414 @@
52 52
53 53 add_filter( 'render_block_context', 'gutenberg_block_core_gallery_render_context', 10, 2 );
54 54
55 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 +/**
56 462 * Renders the `core/gallery` block on the server.
57 463 *
58 464 * @since 6.0.0
59 465 *
@@ -62,82 +468,229 @@
62 468 * @param array $block The block instance being rendered.
63 469 * @return string The content of the block being rendered.
64 470 */
65 471 function gutenberg_block_core_gallery_render( $attributes, $content, $block ) {
66 - // Adds a style tag for the --wp--style--unstable-gallery-gap var.
67 - // The Gallery block needs to recalculate Image block width based on
68 - // the current gap setting in order to maintain the number of flex columns
69 - // so a css var is added to allow this.
472 + static $global_styles = null;
70 473
71 - $gap = $attributes['style']['spacing']['blockGap'] ?? null;
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 - if ( is_array( $gap ) ) {
76 - foreach ( $gap as $key => $value ) {
77 - // Make sure $value is a string to avoid PHP 8.1 deprecation error in preg_match() when the value is null.
78 - $value = is_string( $value ) ? $value : '';
79 - $value = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
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;
80 480
81 - // Get spacing CSS variable from preset value if provided.
82 - if ( is_string( $value ) && str_contains( $value, 'var:preset|spacing|' ) ) {
83 - $index_to_splice = strrpos( $value, '|' ) + 1;
84 - $slug = _wp_to_kebab_case( substr( $value, $index_to_splice ) );
85 - $value = "var(--wp--preset--spacing--$slug)";
86 - }
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 );
87 490
88 - $gap[ $key ] = $value;
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 '';
89 496 }
90 - } else {
91 - // Make sure $gap is a string to avoid PHP 8.1 deprecation error in preg_match() when the value is null.
92 - $gap = is_string( $gap ) ? $gap : '';
93 - $gap = $gap && preg_match( '%[\\\(&=}]|/\*%', $gap ) ? null : $gap;
94 497
95 - // Get spacing CSS variable from preset value if provided.
96 - if ( is_string( $gap ) && str_contains( $gap, 'var:preset|spacing|' ) ) {
97 - $index_to_splice = strrpos( $gap, '|' ) + 1;
98 - $slug = _wp_to_kebab_case( substr( $gap, $index_to_splice ) );
99 - $gap = "var(--wp--preset--spacing--$slug)";
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 );
100 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 );
101 549 }
102 550
103 - $unique_gallery_classname = wp_unique_id( 'wp-block-gallery-' );
104 - $processed_content = new WP_HTML_Tag_Processor( $content );
551 + $processed_content = new WP_HTML_Tag_Processor( $content );
105 552 $processed_content->next_tag();
106 - $processed_content->add_class( $unique_gallery_classname );
107 553
108 - // --gallery-block--gutter-size is deprecated. --wp--style--gallery-gap-default should be used by themes that want to set a default
109 - // gap on the gallery.
110 - $fallback_gap = 'var( --wp--style--gallery-gap-default, var( --gallery-block--gutter-size, var( --wp--style--block-gap, 0.5em ) ) )';
111 - $gap_value = $gap ? $gap : $fallback_gap;
112 - $gap_column = $gap_value;
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 + }
113 564
114 - if ( is_array( $gap_value ) ) {
115 - $gap_row = $gap_value['top'] ?? $fallback_gap;
116 - $gap_column = $gap_value['left'] ?? $fallback_gap;
117 - $gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
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 + }
118 573 }
119 574
120 - // The unstable gallery gap calculation requires a real value (such as `0px`) and not `0`.
121 - if ( '0' === $gap_column ) {
122 - $gap_column = '0px';
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 + }
123 588 }
124 589
125 - // Set the CSS variable to the column value, and the `gap` property to the combined gap value.
126 - $gallery_styles = array(
127 - array(
128 - 'selector' => ".wp-block-gallery.{$unique_gallery_classname}",
129 - 'declarations' => array(
130 - '--wp--style--unstable-gallery-gap' => $gap_column,
131 - 'gap' => $gap_value,
132 - ),
133 - ),
134 - );
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' );
135 597
136 - gutenberg_style_engine_get_stylesheet_from_css_rules(
137 - $gallery_styles,
138 - array( 'context' => 'block-supports' )
139 - );
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 + }
140 693
141 694 // The WP_HTML_Tag_Processor class calls get_updated_html() internally
142 695 // when the instance is treated as a string, but here we explicitly
143 696 // convert it to a string.