diff --git a/plugins/auto-sizes/includes/improve-calculate-sizes.php b/plugins/auto-sizes/includes/improve-calculate-sizes.php index 027f4d1013..b0d9a46024 100644 --- a/plugins/auto-sizes/includes/improve-calculate-sizes.php +++ b/plugins/auto-sizes/includes/improve-calculate-sizes.php @@ -124,6 +124,24 @@ function auto_sizes_filter_image_tag( $content, array $parsed_block, WP_Block $b $size = array( 420, 420 ); } + /* + * When a Gallery block has a wide or full alignment, child Image blocks inherit that + * alignment unless the child block explicitly sets its own alignment. + */ + $is_parent_gallery_aligned = (bool) ( $block->context['is_parent_aligned'] ?? false ); + if ( $is_parent_gallery_aligned && '' === $alignment ) { + $alignment = $max_alignment; + + /* + * A full alignment normally spans the whole viewport, but inside a narrower + * container (e.g. a column) the gallery is constrained to that container instead. + * The resulting width cannot be expressed reliably here, so keep the default sizes. + */ + if ( 1.0 !== (float) $container_relative_width && 'full' === $alignment ) { + return $sizes; + } + } + $better_sizes = auto_sizes_calculate_better_sizes( $id, $size, $alignment, $width, $max_alignment, $container_relative_width ); // If better sizes can't be calculated, use the default sizes. @@ -311,11 +329,12 @@ function auto_sizes_filter_uses_context( array $uses_context, WP_Block_Type $blo // Define block-specific context usage. $block_specific_context = array( 'core/cover' => array( 'max_alignment', 'container_relative_width' ), - 'core/image' => array( 'max_alignment', 'container_relative_width' ), + 'core/image' => array( 'max_alignment', 'container_relative_width', 'is_parent_aligned' ), 'core/post-featured-image' => array( 'max_alignment', 'container_relative_width' ), 'core/group' => array( 'max_alignment' ), 'core/columns' => array( 'max_alignment', 'column_count', 'container_relative_width' ), 'core/column' => array( 'max_alignment' ), + 'core/gallery' => array( 'max_alignment', 'gallery_column_count', 'container_relative_width', 'gallery_total_images' ), ); if ( isset( $block_specific_context[ $block_type->name ] ) ) { @@ -344,6 +363,7 @@ function auto_sizes_filter_render_block_context( array $context, array $block, ? 'core/columns', 'core/group', 'core/post-featured-image', + 'core/gallery', ); if ( in_array( $block['blockName'], $provider_blocks, true ) ) { @@ -355,6 +375,67 @@ function auto_sizes_filter_render_block_context( array $context, array $block, ? $context['max_alignment'] = $constraints[ $context['max_alignment'] ] > $constraints[ $alignment ] ? $context['max_alignment'] : $alignment; } + if ( 'core/gallery' === $block['blockName'] ) { + $gallery_image_block_count = count( $block['innerBlocks'] ); + // Store the total number of inner images in the gallery context for child blocks to access. + $context['gallery_total_images'] = $gallery_image_block_count; + + if ( isset( $block['attrs']['columns'] ) && is_numeric( $block['attrs']['columns'] ) && (int) $block['attrs']['columns'] > 0 ) { + $context['gallery_column_count'] = (int) $block['attrs']['columns']; + } elseif ( $gallery_image_block_count <= 3 ) { + /* + * Fallback to the inner block count if column count isn't explicitly set. + * Cap the columns at 3 to prevent layout issues, as the default gallery + * style wraps images to new lines after 3 columns. + * See https://github.com/WordPress/gutenberg/blob/46e0ceee86b66a6036c9e58568ce21bc1cf8b630/packages/block-library/src/gallery/style.scss#L167 + */ + $context['gallery_column_count'] = $gallery_image_block_count; + } else { + $context['gallery_column_count'] = 3; + } + + // If the gallery is wide or full aligned, treat child Image blocks as aligned too. + $gallery_alignment = $block['attrs']['align'] ?? ''; + $context['is_parent_aligned'] = in_array( $gallery_alignment, array( 'wide', 'full' ), true ); + } + + // Special handling for images inside galleries, as they have a different layout calculation that depends on the number of columns. + if ( 'core/image' === $block['blockName'] && $parent_block instanceof WP_Block && 'core/gallery' === $parent_block->parsed_block['blockName'] ) { + $columns = $parent_block->context['gallery_column_count'] ?? 0; + $total_images = $parent_block->context['gallery_total_images'] ?? 0; + + $current_width = 1.0; + + if ( $total_images > 0 && $columns > 0 ) { + // Sequential rendering tracker. + static $last_parent_gallery = null; + static $image_index = 0; + + // If the parent block object changes, reset the index for the new gallery. + if ( $last_parent_gallery !== $parent_block ) { + $last_parent_gallery = $parent_block; + $image_index = 0; + } + + $remainder = $total_images % $columns; + $full_rows_images_count = $total_images - $remainder; + + if ( $remainder > 0 && $image_index >= $full_rows_images_count ) { + // Image falls in the last, incomplete row. Distribute evenly among leftovers. + $current_width = 1.0 / $remainder; + } else { + // Image falls within the perfect full grid rows. + $current_width = 1.0 / $columns; + } + + // Move to the next image index for the next iteration. + ++$image_index; + } + + // Multiply with parent's width if available. + $context['container_relative_width'] = ( $parent_block->context['container_relative_width'] ?? 1 ) * $current_width; + } + if ( 'core/columns' === $block['blockName'] ) { // This is a special context key just to pass to the child 'core/column' block. $context['column_count'] = count( $block['innerBlocks'] ); diff --git a/plugins/auto-sizes/tests/test-improve-calculate-sizes.php b/plugins/auto-sizes/tests/test-improve-calculate-sizes.php index e5768a1a8d..0489b25531 100644 --- a/plugins/auto-sizes/tests/test-improve-calculate-sizes.php +++ b/plugins/auto-sizes/tests/test-improve-calculate-sizes.php @@ -1475,6 +1475,804 @@ public function test_auto_sizes_get_featured_image_attachment_id_returns_zero_fo ); } + /** + * Test that the Gallery block dynamically calculates and outputs the correct responsive + * 'sizes' attribute based on the number of images and the configured automatic column count. + * + * @dataProvider data_gallery_block_with_automatic_columns_set + * + * @param string $ancestor_block_alignment Ancestor block alignment. + * @param string $image_block_alignment Image block alignment. + * @param int $image_count Number of images. + * @param string $expected Expected output. + */ + public function test_gallery_block_with_automatic_columns_set( string $ancestor_block_alignment, string $image_block_alignment, int $image_count, string $expected ): void { + $image_block = ''; + for ( $i = 0; $i < $image_count; $i++ ) { + $image_block .= $this->get_image_block_markup( self::$image_id, 'large', $image_block_alignment ); + } + $block_content = $this->get_gallery_block_markup( + $image_block, + array( + 'align' => $ancestor_block_alignment, + ) + ); + + $result = apply_filters( 'the_content', $block_content ); + $this->assertSame( $image_count, substr_count( $result, $expected ), 'Expected all gallery images to have the same calculated sizes attribute.' ); + } + + /** + * Data provider for testing Gallery block layout sizes with automatic columns set. + * + * @return array> Arguments passed to the test method. + */ + public static function data_gallery_block_with_automatic_columns_set(): array { + return array( + 'Default alignment, 1 image (1 column)' => array( + '', + '', + 1, + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 1 image (1 column)' => array( + 'wide', + '', + 1, + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 2 images (2 columns)' => array( + '', + '', + 2, + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 2 images (2 columns)' => array( + 'wide', + '', + 2, + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 3 images (3 columns)' => array( + '', + '', + 3, + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 3 images (3 columns)' => array( + 'wide', + '', + 3, + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 6 images (maxes out column constraints)' => array( + '', + '', + 6, + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 6 images (maxes out column constraints)' => array( + 'wide', + '', + 6, + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 9 images (maxes out column constraints)' => array( + '', + '', + 9, + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 9 images (maxes out column constraints)' => array( + 'wide', + '', + 9, + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + + /* + * A full aligned gallery with a single image spans the whole viewport, + * so the image covers '100vw'. Once the gallery has more than one column + * the image no longer spans the viewport and the default sizes are used. + */ + 'Full gallery alignment, Default image alignment, 1 image (1 column)' => array( + 'full', + '', + 1, + 'sizes="100vw" ', + ), + 'Full gallery alignment, Default image alignment, 2 images (2 columns)' => array( + 'full', + '', + 2, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Full gallery alignment, Default image alignment, 3 images (3 columns)' => array( + 'full', + '', + 3, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Full gallery alignment, Default image alignment, 6 images (maxes out column constraints)' => array( + 'full', + '', + 6, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Full gallery alignment, Default image alignment, 9 images (maxes out column constraints)' => array( + 'full', + '', + 9, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + ); + } + + /** + * Test that the Gallery block dynamically calculates and outputs the correct responsive + * 'sizes' attribute based on the number of images and columns. + * + * @dataProvider data_gallery_block_with_columns_set + * + * @param string $ancestor_block_alignment Ancestor block alignment. + * @param string $image_block_alignment Image block alignment. + * @param int $image_count Number of images. + * @param int $columns Number of columns in the gallery. + * @param string $big_img_expected Expected sizes attribute output for large images. + * @param string $small_img_expected Expected sizes attribute output for small images (optional). + */ + public function test_gallery_block_with_columns_set( string $ancestor_block_alignment, string $image_block_alignment, int $image_count, int $columns, string $big_img_expected, string $small_img_expected = '' ): void { + $image_block = ''; + for ( $i = 0; $i < $image_count; $i++ ) { + $image_block .= $this->get_image_block_markup( self::$image_id, 'large', $image_block_alignment ); + } + $block_content = $this->get_gallery_block_markup( + $image_block, + array( + 'align' => $ancestor_block_alignment, + 'columns' => $columns, + ) + ); + + $result = apply_filters( 'the_content', $block_content ); + + if ( '' === $small_img_expected || $small_img_expected === $big_img_expected ) { + $this->assertSame( $image_count, substr_count( $result, $big_img_expected ) ); + } else { + $remainder = $columns > 0 ? ( $image_count % $columns ) : 0; + $small_expected_count = 0 === $remainder ? 0 : $remainder; + $big_expected_count = $image_count - $small_expected_count; + $this->assertSame( $big_expected_count, substr_count( $result, $big_img_expected ) ); + $this->assertSame( $small_expected_count, substr_count( $result, $small_img_expected ) ); + } + } + + /** + * Data provider for testing Gallery block layout sizes with columns set. + * + * @return array> Arguments passed to the test method. + */ + public static function data_gallery_block_with_columns_set(): array { + return array( + // Equal image and columns. + 'Default alignment, 1 image (1 column)' => array( + '', + '', + 1, + 1, + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 1 image (1 column)' => array( + 'wide', + '', + 1, + 1, + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 2 images (2 columns)' => array( + '', + '', + 2, + 2, + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 2 images (2 columns)' => array( + 'wide', + '', + 2, + 2, + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 3 images (3 columns)' => array( + '', + '', + 3, + 3, + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 3 images (3 columns)' => array( + 'wide', + '', + 3, + 3, + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 4 images (4 columns)' => array( + '', + '', + 4, + 4, + 'sizes="(max-width: 155px) 100vw, 155px" ', + ), + 'Wide gallery alignment, Default image alignment, 4 images (4 columns)' => array( + 'wide', + '', + 4, + 4, + 'sizes="(max-width: 320px) 100vw, 320px" ', + ), + 'Default alignment, 5 images (5 columns)' => array( + '', + '', + 5, + 5, + 'sizes="(max-width: 124px) 100vw, 124px" ', + ), + 'Wide gallery alignment, Default image alignment, 5 images (5 columns)' => array( + 'wide', + '', + 5, + 5, + 'sizes="(max-width: 256px) 100vw, 256px" ', + ), + 'Default alignment, 6 images (6 columns)' => array( + '', + '', + 6, + 6, + 'sizes="(max-width: 103px) 100vw, 103px" ', + ), + 'Wide gallery alignment, Default image alignment, 6 images (6 columns)' => array( + 'wide', + '', + 6, + 6, + 'sizes="(max-width: 213px) 100vw, 213px" ', + ), + 'Default alignment, 7 images (7 columns)' => array( + '', + '', + 7, + 7, + 'sizes="(max-width: 88px) 100vw, 88px" ', + ), + 'Wide gallery alignment, Default image alignment, 7 images (7 columns)' => array( + 'wide', + '', + 7, + 7, + 'sizes="(max-width: 182px) 100vw, 182px" ', + ), + 'Default alignment, 8 images (8 columns)' => array( + '', + '', + 8, + 8, + 'sizes="(max-width: 77px) 100vw, 77px" ', + ), + 'Wide gallery alignment, Default image alignment, 8 images (8 columns)' => array( + 'wide', + '', + 8, + 8, + 'sizes="(max-width: 160px) 100vw, 160px" ', + ), + + // Different combination of image and columns. + 'Default alignment, 2 images (1 columns)' => array( + '', + '', + 2, + 1, + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 2 images (1 column)' => array( + 'wide', + '', + 2, + 1, + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 3 image (2 columns)' => array( + '', + '', + 3, + 2, + 'sizes="(max-width: 310px) 100vw, 310px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 3 images (2 columns)' => array( + 'wide', + '', + 3, + 2, + 'sizes="(max-width: 640px) 100vw, 640px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 4 image (2 columns)' => array( + '', + '', + 4, + 2, + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 4 images (2 columns)' => array( + 'wide', + '', + 4, + 2, + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 5 image (2 columns)' => array( + '', + '', + 5, + 2, + 'sizes="(max-width: 310px) 100vw, 310px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 5 images (2 columns)' => array( + 'wide', + '', + 5, + 2, + 'sizes="(max-width: 640px) 100vw, 640px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 4 image (3 columns)' => array( + '', + '', + 4, + 3, + 'sizes="(max-width: 206px) 100vw, 206px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 4 images (3 columns)' => array( + 'wide', + '', + 4, + 3, + 'sizes="(max-width: 426px) 100vw, 426px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 5 image (3 columns)' => array( + '', + '', + 5, + 3, + 'sizes="(max-width: 206px) 100vw, 206px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 5 image (3 columns)' => array( + 'wide', + '', + 5, + 3, + 'sizes="(max-width: 426px) 100vw, 426px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 6 image (3 columns)' => array( + '', + '', + 6, + 3, + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 6 image (3 columns)' => array( + 'wide', + '', + 6, + 3, + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 5 image (4 columns)' => array( + '', + '', + 5, + 4, + 'sizes="(max-width: 155px) 100vw, 155px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 5 image (4 columns)' => array( + 'wide', + '', + 5, + 4, + 'sizes="(max-width: 320px) 100vw, 320px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 6 image (4 columns)' => array( + '', + '', + 6, + 4, + 'sizes="(max-width: 155px) 100vw, 155px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 6 image (4 columns)' => array( + 'wide', + '', + 6, + 4, + 'sizes="(max-width: 320px) 100vw, 320px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 7 image (4 columns)' => array( + '', + '', + 7, + 4, + 'sizes="(max-width: 155px) 100vw, 155px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 7 image (4 columns)' => array( + 'wide', + '', + 7, + 4, + 'sizes="(max-width: 320px) 100vw, 320px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 6 image (5 columns)' => array( + '', + '', + 6, + 5, + 'sizes="(max-width: 124px) 100vw, 124px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 6 image (5 columns)' => array( + 'wide', + '', + 6, + 5, + 'sizes="(max-width: 256px) 100vw, 256px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 7 image (5 columns)' => array( + '', + '', + 7, + 5, + 'sizes="(max-width: 124px) 100vw, 124px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 7 image (5 columns)' => array( + 'wide', + '', + 7, + 5, + 'sizes="(max-width: 256px) 100vw, 256px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 8 image (5 columns)' => array( + '', + '', + 8, + 5, + 'sizes="(max-width: 124px) 100vw, 124px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 8 image (5 columns)' => array( + 'wide', + '', + 8, + 5, + 'sizes="(max-width: 256px) 100vw, 256px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 7 image (6 columns)' => array( + '', + '', + 7, + 6, + 'sizes="(max-width: 103px) 100vw, 103px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 7 image (6 columns)' => array( + 'wide', + '', + 7, + 6, + 'sizes="(max-width: 213px) 100vw, 213px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 8 image (6 columns)' => array( + '', + '', + 8, + 6, + 'sizes="(max-width: 103px) 100vw, 103px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 8 image (6 columns)' => array( + 'wide', + '', + 8, + 6, + 'sizes="(max-width: 213px) 100vw, 213px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 9 image (6 columns)' => array( + '', + '', + 9, + 6, + 'sizes="(max-width: 103px) 100vw, 103px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 9 image (6 columns)' => array( + 'wide', + '', + 9, + 6, + 'sizes="(max-width: 213px) 100vw, 213px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 8 image (7 columns)' => array( + '', + '', + 8, + 7, + 'sizes="(max-width: 88px) 100vw, 88px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 8 image (7 columns)' => array( + 'wide', + '', + 8, + 7, + 'sizes="(max-width: 182px) 100vw, 182px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 9 image (7 columns)' => array( + '', + '', + 9, + 7, + 'sizes="(max-width: 88px) 100vw, 88px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 9 image (7 columns)' => array( + 'wide', + '', + 9, + 7, + 'sizes="(max-width: 182px) 100vw, 182px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 10 image (7 columns)' => array( + '', + '', + 10, + 7, + 'sizes="(max-width: 88px) 100vw, 88px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 10 image (7 columns)' => array( + 'wide', + '', + 10, + 7, + 'sizes="(max-width: 182px) 100vw, 182px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + 'Default alignment, 9 image (8 columns)' => array( + '', + '', + 9, + 8, + 'sizes="(max-width: 77px) 100vw, 77px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Wide gallery alignment, Default image alignment, 9 image (8 columns)' => array( + 'wide', + '', + 9, + 8, + 'sizes="(max-width: 160px) 100vw, 160px" ', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Default alignment, 10 image (8 columns)' => array( + '', + '', + 10, + 8, + 'sizes="(max-width: 77px) 100vw, 77px" ', + 'sizes="(max-width: 310px) 100vw, 310px" ', + ), + 'Wide gallery alignment, Default image alignment, 10 image (8 columns)' => array( + 'wide', + '', + 10, + 8, + 'sizes="(max-width: 160px) 100vw, 160px" ', + 'sizes="(max-width: 640px) 100vw, 640px" ', + ), + 'Default alignment, 11 image (8 columns)' => array( + '', + '', + 11, + 8, + 'sizes="(max-width: 77px) 100vw, 77px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 11 image (8 columns)' => array( + 'wide', + '', + 11, + 8, + 'sizes="(max-width: 160px) 100vw, 160px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + + // Random image columns setup. + 'Default alignment, 15 image (5 columns)' => array( + '', + '', + 15, + 5, + 'sizes="(max-width: 124px) 100vw, 124px" ', + ), + 'Wide gallery alignment, Default image alignment, 15 image (5 columns)' => array( + 'wide', + '', + 15, + 5, + 'sizes="(max-width: 256px) 100vw, 256px" ', + ), + 'Default alignment, 15 image (6 columns)' => array( + '', + '', + 15, + 6, + 'sizes="(max-width: 103px) 100vw, 103px" ', + 'sizes="(max-width: 206px) 100vw, 206px" ', + ), + 'Wide gallery alignment, Default image alignment, 15 image (6 columns)' => array( + 'wide', + '', + 15, + 6, + 'sizes="(max-width: 213px) 100vw, 213px" ', + 'sizes="(max-width: 426px) 100vw, 426px" ', + ), + + /* + * Full aligned galleries. Only a single column spans the whole viewport, + * every other column count falls back to the default sizes, including the + * images in an incomplete last row. + */ + 'Full gallery alignment, Default image alignment, 1 image (1 column)' => array( + 'full', + '', + 1, + 1, + 'sizes="100vw" ', + ), + 'Full gallery alignment, Default image alignment, 3 images (1 column)' => array( + 'full', + '', + 3, + 1, + 'sizes="100vw" ', + ), + 'Full gallery alignment, Default image alignment, 3 images (2 columns; incomplete last row with remainder=1)' => array( + 'full', + '', + 3, + 2, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + 'sizes="100vw" ', + ), + 'Full gallery alignment, Default image alignment, 4 images (2 columns)' => array( + 'full', + '', + 4, + 2, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Full gallery alignment, Default image alignment, 5 images (3 columns)' => array( + 'full', + '', + 5, + 3, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Full gallery alignment, Default image alignment, 15 images (6 columns)' => array( + 'full', + '', + 15, + 6, + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + ); + } + + /** + * Test that a Gallery block only marks its child Image blocks as aligned for + * the alignments that actually break out of the content width. + * + * @dataProvider data_gallery_is_parent_aligned_context + * + * @cover ::auto_sizes_filter_render_block_context + * + * @param string $gallery_alignment Gallery block alignment. + * @param bool $expected Expected 'is_parent_aligned' context value. + */ + public function test_gallery_is_parent_aligned_context( string $gallery_alignment, bool $expected ): void { + $block = array( + 'blockName' => 'core/gallery', + 'attrs' => '' !== $gallery_alignment ? array( 'align' => $gallery_alignment ) : array(), + 'innerBlocks' => array(), + ); + + $context = auto_sizes_filter_render_block_context( array(), $block, null ); + + $this->assertArrayHasKey( 'is_parent_aligned', $context ); + $this->assertSame( $expected, $context['is_parent_aligned'] ); + } + + /** + * Data provider for testing the 'is_parent_aligned' gallery context. + * + * @return array Arguments passed to the test method. + */ + public static function data_gallery_is_parent_aligned_context(): array { + return array( + 'wide alignment' => array( 'wide', true ), + 'full alignment' => array( 'full', true ), + 'no alignment' => array( '', false ), + 'left alignment' => array( 'left', false ), + 'right alignment' => array( 'right', false ), + 'center alignment' => array( 'center', false ), + ); + } + + /** + * Test that a non gallery block never receives the 'is_parent_aligned' context, + * even when it is full aligned. + * + * @cover ::auto_sizes_filter_render_block_context + */ + public function test_non_gallery_block_does_not_receive_is_parent_aligned_context(): void { + $block = array( + 'blockName' => 'core/group', + 'attrs' => array( 'align' => 'full' ), + 'innerBlocks' => array(), + ); + + $context = auto_sizes_filter_render_block_context( array(), $block, null ); + + $this->assertArrayNotHasKey( 'is_parent_aligned', $context ); + } + + /** + * Test that the Image block declares the context keys the gallery alignment + * handling depends on. + * + * @cover ::auto_sizes_filter_uses_context + */ + public function test_image_block_uses_is_parent_aligned_context(): void { + $block_type = new WP_Block_Type( 'core/image' ); + + $uses_context = auto_sizes_filter_uses_context( array(), $block_type ); + + $this->assertContains( 'is_parent_aligned', $uses_context ); + $this->assertContains( 'max_alignment', $uses_context ); + $this->assertContains( 'container_relative_width', $uses_context ); + } + /** * Filter the theme.json data to include relative layout sizes. * @@ -1598,4 +2396,36 @@ public function get_columns_block_markup( string $content, array $atts = array() $column_block ); } + + /** + * Helper to generate gallery block markup. + * + * This function generates a WordPress gallery block with optional alignment. + * + * @param string $content Content to be included in the gallery. + * @param array $atts Optional. Block attributes. Default empty array. + * @return string The generated gallery block markup. + */ + public function get_gallery_block_markup( string $content, array $atts = array() ): string { + $atts = wp_parse_args( + $atts, + array( + 'linkTo' => 'none', + ) + ); + + $class = ''; + $class .= ( isset( $atts['align'] ) && '' !== $atts['align'] ) ? ' align' . $atts['align'] : ''; + $class .= ' columns-' . ( ( isset( $atts['columns'] ) && '' !== $atts['columns'] ) ? $atts['columns'] : 'default' ); + + // Generate and return the final gallery block markup. + return sprintf( + ' + + ', + $content + ); + } }