diff --git a/plugins/auto-sizes/hooks.php b/plugins/auto-sizes/hooks.php index 45612d9af..55cbe9dec 100644 --- a/plugins/auto-sizes/hooks.php +++ b/plugins/auto-sizes/hooks.php @@ -38,3 +38,5 @@ function auto_sizes_render_generator(): void { add_filter( 'the_content', 'auto_sizes_prime_attachment_caches', 9 ); // This must run before 'do_blocks', which runs at priority 9. add_filter( 'render_block_core/image', 'auto_sizes_filter_image_tag', 10, 3 ); add_filter( 'render_block_core/cover', 'auto_sizes_filter_image_tag', 10, 3 ); +add_filter( 'get_block_type_uses_context', 'auto_sizes_filter_uses_context', 10, 2 ); +add_filter( 'render_block_context', 'auto_sizes_filter_render_block_context', 10, 2 ); diff --git a/plugins/auto-sizes/includes/improve-calculate-sizes.php b/plugins/auto-sizes/includes/improve-calculate-sizes.php index 61df526a2..40b50a0e4 100644 --- a/plugins/auto-sizes/includes/improve-calculate-sizes.php +++ b/plugins/auto-sizes/includes/improve-calculate-sizes.php @@ -6,25 +6,6 @@ * @since n.e.x.t */ -/** - * Gets the smaller image size if the layout width is bigger. - * - * It will return the smaller image size and return "px" if the layout width - * is something else, e.g. min(640px, 90vw) or 90vw. - * - * @since 1.1.0 - * - * @param string $layout_width The layout width. - * @param int $image_width The image width. - * @return string The proper width after some calculations. - */ -function auto_sizes_get_width( string $layout_width, int $image_width ): string { - if ( str_ends_with( $layout_width, 'px' ) ) { - return $image_width > (int) $layout_width ? $layout_width : $image_width . 'px'; - } - return $image_width . 'px'; -} - /** * Primes attachment into the cache with a single database query. * @@ -84,6 +65,7 @@ function auto_sizes_filter_image_tag( $content, array $parsed_block, WP_Block $b if ( ! is_string( $content ) ) { return ''; } + $processor = new WP_HTML_Tag_Processor( $content ); $has_image = $processor->next_tag( array( 'tag_name' => 'IMG' ) ); @@ -99,11 +81,24 @@ function auto_sizes_filter_image_tag( $content, array $parsed_block, WP_Block $b * @param string $size The image size data. */ $filter = static function ( $sizes, $size ) use ( $block ) { - $id = $block->attributes['id'] ?? 0; - $alignment = $block->attributes['align'] ?? ''; - $width = $block->attributes['width'] ?? ''; - return auto_sizes_calculate_better_sizes( (int) $id, (string) $size, (string) $alignment, (string) $width ); + $id = isset( $block->attributes['id'] ) ? (int) $block->attributes['id'] : 0; + $alignment = $block->attributes['align'] ?? ''; + $width = isset( $block->attributes['width'] ) ? (int) $block->attributes['width'] : 0; + $max_alignment = $block->context['max_alignment'] ?? ''; + + /* + * Update width for cover block. + * See https://github.com/WordPress/gutenberg/blob/938720602082dc50a1746bd2e33faa3d3a6096d4/packages/block-library/src/cover/style.scss#L82-L87. + */ + if ( 'core/cover' === $block->name && in_array( $alignment, array( 'left', 'right' ), true ) ) { + $size = array( 420, 420 ); + } + + $better_sizes = auto_sizes_calculate_better_sizes( $id, $size, $alignment, $width, $max_alignment ); + + // If better sizes can't be calculated, use the default sizes. + return false !== $better_sizes ? $better_sizes : $sizes; }; // Hook this filter early, before default filters are run. @@ -135,50 +130,175 @@ function auto_sizes_filter_image_tag( $content, array $parsed_block, WP_Block $b /** * Modifies the sizes attribute of an image based on layout context. * - * @param int $id The image id. - * @param string $size The image size data. - * @param string $align The image alignment. - * @param string $resize_width Resize image width. - * @return string The sizes attribute value. + * @since n.e.x.t + * + * @param int $id The image attachment post ID. + * @param string|array{int, int} $size Image size name or array of width and height. + * @param string $align The image alignment. + * @param int $resize_width Resize image width. + * @param string $max_alignment The maximum usable layout alignment. + * @return string|false An improved sizes attribute or false if a better size cannot be calculated. */ -function auto_sizes_calculate_better_sizes( int $id, string $size, string $align, string $resize_width ): string { - $sizes = ''; - $image = wp_get_attachment_image_src( $id, $size ); +function auto_sizes_calculate_better_sizes( int $id, $size, string $align, int $resize_width, string $max_alignment ) { + // Without an image ID or a resize width, we cannot calculate a better size. + if ( 0 === $id && 0 === $resize_width ) { + return false; + } - if ( false === $image ) { - return $sizes; + $image_data = wp_get_attachment_image_src( $id, $size ); + + $image_width = false !== $image_data ? $image_data[1] : 0; + + // If we don't have an image width or a resize width, we cannot calculate a better size. + if ( 0 === $image_width && 0 === $resize_width ) { + return false; } - // Retrieve width from the image tag itself. - $image_width = '' !== $resize_width ? (int) $resize_width : $image[1]; + /* + * If we don't have an image width, use the resize width. + * If we have both an image width and a resize width, use the smaller of the two. + */ + if ( 0 === $image_width ) { + $image_width = $resize_width; + } elseif ( 0 !== $resize_width ) { + $image_width = min( $image_width, $resize_width ); + } + + // Normalize default alignment values. + $align = '' !== $align ? $align : 'default'; + + /* + * Map alignment values to a weighting value so they can be compared. + * Note that 'left' and 'right' alignments are only constrained by max alignment. + */ + $constraints = array( + 'full' => 0, + 'wide' => 1, + 'left' => 2, + 'right' => 2, + 'default' => 3, + 'center' => 3, + ); - $layout = wp_get_global_settings( array( 'layout' ) ); + $alignment = $constraints[ $align ] > $constraints[ $max_alignment ] ? $align : $max_alignment; // Handle different alignment use cases. - switch ( $align ) { + switch ( $alignment ) { case 'full': - $sizes = '100vw'; + $layout_width = auto_sizes_get_layout_width( 'full' ); break; case 'wide': - if ( array_key_exists( 'wideSize', $layout ) ) { - $sizes = sprintf( '(max-width: %1$s) 100vw, %1$s', $layout['wideSize'] ); - } + $layout_width = auto_sizes_get_layout_width( 'wide' ); break; case 'left': case 'right': - case 'center': - $sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $image_width ); + $layout_width = sprintf( '%1$spx', $image_width ); break; + case 'center': default: - if ( array_key_exists( 'contentSize', $layout ) ) { - $width = auto_sizes_get_width( $layout['contentSize'], $image_width ); - $sizes = sprintf( '(max-width: %1$s) 100vw, %1$s', $width ); - } + $alignment = auto_sizes_get_layout_width( 'default' ); + $layout_width = sprintf( '%1$spx', min( (int) $alignment, $image_width ) ); break; } - return $sizes; + // Format layout width when not 'full'. + if ( 'full' !== $alignment ) { + $layout_width = sprintf( '(max-width: %1$s) 100vw, %1$s', $layout_width ); + } + + return $layout_width; +} + +/** + * Retrieves the layout width for an alignment defined in theme.json. + * + * @since n.e.x.t + * + * @param string $alignment The alignment value. + * @return string The alignment width based. + */ +function auto_sizes_get_layout_width( string $alignment ): string { + $layout = auto_sizes_get_layout_settings(); + + $layout_widths = array( + 'full' => '100vw', // Todo: incorporate useRootPaddingAwareAlignments. + 'wide' => array_key_exists( 'wideSize', $layout ) ? $layout['wideSize'] : '', + 'default' => array_key_exists( 'contentSize', $layout ) ? $layout['contentSize'] : '', + ); + + return $layout_widths[ $alignment ] ?? ''; +} + +/** + * Filters the context keys that a block type uses. + * + * @since n.e.x.t + * + * @param string[] $uses_context Array of registered uses context for a block type. + * @param WP_Block_Type $block_type The full block type object. + * @return string[] The filtered context keys used by the block type. + */ +function auto_sizes_filter_uses_context( array $uses_context, WP_Block_Type $block_type ): array { + // The list of blocks that can consume outer layout context. + $consumer_blocks = array( + 'core/cover', + 'core/image', + ); + + if ( in_array( $block_type->name, $consumer_blocks, true ) ) { + // Use array_values to reset the array keys after merging. + return array_values( array_unique( array_merge( $uses_context, array( 'max_alignment' ) ) ) ); + } + return $uses_context; +} + +/** + * Modifies the block context during rendering to blocks. + * + * @since n.e.x.t + * + * @param array $context Current block context. + * @param array $block The block being rendered. + * @return array Modified block context. + */ +function auto_sizes_filter_render_block_context( array $context, array $block ): array { + // When no max alignment is set, the maximum is assumed to be 'full'. + $context['max_alignment'] = $context['max_alignment'] ?? 'full'; + + // The list of blocks that can modify outer layout context. + $provider_blocks = array( + 'core/columns', + 'core/group', + ); + + if ( in_array( $block['blockName'], $provider_blocks, true ) ) { + $alignment = $block['attrs']['align'] ?? ''; + + // If the container block doesn't have alignment, it's assumed to be 'default'. + if ( '' === $alignment ) { + $context['max_alignment'] = 'default'; + } elseif ( 'wide' === $alignment ) { + $context['max_alignment'] = 'wide'; + } + } + + return $context; +} + +/** + * Retrieves the layout settings defined in theme.json. + * + * @since n.e.x.t + * + * @return array Associative array of layout settings. + */ +function auto_sizes_get_layout_settings(): array { + static $layout = array(); + if ( count( $layout ) === 0 ) { + $layout = wp_get_global_settings( array( 'layout' ) ); + } + return $layout; } diff --git a/plugins/auto-sizes/tests/test-improve-calculate-sizes.php b/plugins/auto-sizes/tests/test-improve-calculate-sizes.php index 1e8bd819c..1ef56fd2c 100644 --- a/plugins/auto-sizes/tests/test-improve-calculate-sizes.php +++ b/plugins/auto-sizes/tests/test-improve-calculate-sizes.php @@ -62,7 +62,7 @@ public function test_that_if_disable_responsive_image_then_it_will_not_add_sizes * @param string $image_size Image size. */ public function test_image_block_with_full_alignment( string $image_size ): void { - $block_content = '
'; + $block_content = $this->get_image_block_markup( self::$image_id, $image_size, 'full' ); $result = apply_filters( 'the_content', $block_content ); @@ -93,7 +93,7 @@ public function test_cover_block_with_full_alignment(): void { * @param string $image_size Image size. */ public function test_image_block_with_wide_alignment( string $image_size ): void { - $block_content = '
'; + $block_content = $this->get_image_block_markup( self::$image_id, $image_size, 'wide' ); $result = apply_filters( 'the_content', $block_content ); @@ -273,14 +273,14 @@ public function data_image_sizes_for_left_right_center_alignment(): array { 'sizes="(max-width: 300px) 100vw, 300px" ', 'center', ), - 'Return large image size 1024px with center alignment' => array( + 'Return large image size 620px with center alignment' => array( 'large', - 'sizes="(max-width: 1024px) 100vw, 1024px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', 'center', ), - 'Return full image size 1080px with center alignment' => array( + 'Return full image size 620px with center alignment' => array( 'full', - 'sizes="(max-width: 1080px) 100vw, 1080px" ', + 'sizes="(max-width: 620px) 100vw, 620px" ', 'center', ), 'Return resized size 100px instead of medium image size 300px with left alignment' => array( @@ -341,13 +341,14 @@ public function data_image_sizes_for_left_right_center_alignment(): array { } /** - * Test the cover block with left and right alignment. + * Test the cover block with left, right and center alignment. * * @dataProvider data_image_left_right_center_alignment * * @param string $alignment Alignment of the image. + * @param string $expected Expected output. */ - public function test_cover_block_with_left_right_center_alignment( string $alignment ): void { + public function test_cover_block_with_left_right_center_alignment( string $alignment, string $expected ): void { $image_url = wp_get_attachment_image_url( self::$image_id, 'full' ); $block_content = '
@@ -357,7 +358,7 @@ public function test_cover_block_with_left_right_center_alignment( string $align $result = apply_filters( 'the_content', $block_content ); - $this->assertStringContainsString( 'sizes="(max-width: 1080px) 100vw, 1080px" ', $result ); + $this->assertStringContainsString( $expected, $result ); } /** @@ -367,9 +368,9 @@ public function test_cover_block_with_left_right_center_alignment( string $align */ public function data_image_left_right_center_alignment(): array { return array( - array( 'left' ), - array( 'right' ), - array( 'center' ), + array( 'left', 'sizes="(max-width: 420px) 100vw, 420px' ), + array( 'right', 'sizes="(max-width: 420px) 100vw, 420px' ), + array( 'center', 'sizes="(max-width: 620px) 100vw, 620px' ), ); } @@ -385,4 +386,178 @@ public function test_no_image(): void { $this->assertStringContainsString( '

No image here

', $result ); } + + /** + * Test that the layout property of a group block is passed by context to the image block. + * + * @dataProvider data_ancestor_and_image_block_alignment + * + * @param string $ancestor_block_alignment Ancestor block alignment. + * @param string $image_block_alignment Image block alignment. + * @param string $expected Expected output. + */ + public function test_ancestor_layout_is_passed_by_context( string $ancestor_block_alignment, string $image_block_alignment, string $expected ): void { + $block_content = $this->get_group_block_markup( + $this->get_image_block_markup( self::$image_id, 'large', $image_block_alignment ), + array( + 'align' => $ancestor_block_alignment, + ) + ); + + $result = apply_filters( 'the_content', $block_content ); + + $this->assertStringContainsString( $expected, $result ); + } + + /** + * Data provider. + * + * @return array> The ancestor and image alignments. + */ + public function data_ancestor_and_image_block_alignment(): array { + return array( + // Parent default alignment. + 'Return contentSize 620px, parent block default alignment, image block default alignment' => array( + '', + '', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return contentSize 620px, parent block default alignment, image block wide alignment' => array( + '', + 'wide', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return contentSize 620px, parent block default alignment, image block full alignment' => array( + '', + 'full', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return contentSize 620px, parent block default alignment, image block left alignment' => array( + '', + 'left', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return contentSize 620px, parent block default alignment, image block center alignment' => array( + '', + 'center', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return contentSize 620px, parent block default alignment, image block right alignment' => array( + '', + 'right', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + + // Parent wide alignment. + 'Return contentSize 620px, parent block wide alignment, image block default alignment' => array( + 'wide', + '', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return wideSize 1280px, parent block wide alignment, image block wide alignment' => array( + 'wide', + 'wide', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Return wideSize 1280px, parent block wide alignment, image block full alignment' => array( + 'wide', + 'full', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Return image size 1024px, parent block wide alignment, image block left alignment' => array( + 'wide', + 'left', + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Return image size 620px, parent block wide alignment, image block center alignment' => array( + 'wide', + 'center', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return image size 1024px, parent block wide alignment, image block right alignment' => array( + 'wide', + 'right', + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + + // Parent full alignment. + 'Return contentSize 620px, parent block full alignment, image block default alignment' => array( + 'full', + '', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return wideSize 1280px, parent block full alignment, image block wide alignment' => array( + 'full', + 'wide', + 'sizes="(max-width: 1280px) 100vw, 1280px" ', + ), + 'Return full size, parent block full alignment, image block full alignment' => array( + 'full', + 'full', + 'sizes="100vw" ', + ), + 'Return image size 1024px, parent block full alignment, image block left alignment' => array( + 'full', + 'left', + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + 'Return image size 620px, parent block full alignment, image block center alignment' => array( + 'full', + 'center', + 'sizes="(max-width: 620px) 100vw, 620px" ', + ), + 'Return image size 1024px, parent block full alignment, image block right alignment' => array( + 'full', + 'right', + 'sizes="(max-width: 1024px) 100vw, 1024px" ', + ), + ); + } + + /** + * Helper to generate image block markup. + * + * @param int $attachment_id Attachment ID. + * @param string $size Optional. Image size. Default 'full'. + * @param string $align Optional. Image alignment. Default null. + * @return string Image block markup. + */ + public function get_image_block_markup( int $attachment_id, string $size = 'full', string $align = null ): string { + $image_url = wp_get_attachment_image_url( $attachment_id, $size ); + + $atts = array( + 'id' => $attachment_id, + 'sizeSlug' => $size, + 'align' => $align, + 'linkDestination' => 'none', + ); + + $align_class = null !== $align ? ' align' . $align : ''; + + return '
'; + } + + /** + * Helper to generate group block markup. + * + * @param string $content Block content. + * @param array $atts Optional. Block attributes. Default empty array. + * @return string Group block markup. + */ + public function get_group_block_markup( string $content, array $atts = array() ): string { + $atts = wp_parse_args( + $atts, + array( + 'layout' => array( + 'type' => 'constrained', + ), + ) + ); + + $align_class = (bool) $atts['align'] ? ' align' . $atts['align'] : ''; + + return ' +
' . $content . '
+ '; + } }