From 7ba001f69a8bbfe60436175a9e65275105912663 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 27 Aug 2024 15:30:55 +0530 Subject: [PATCH 1/2] Update media.php --- src/wp-includes/media.php | 69 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 50c85c8990f2c..d0b6e591ed8a4 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -1137,6 +1137,15 @@ function wp_get_attachment_image( $attachment_id, $size = 'thumbnail', $icon = f } } + // Adds auto to the sizes attribute if applicable. + if ( + ( isset( $attr['loading'] ) && 'lazy' === $attr['loading'] ) && + isset( $attr['sizes'] ) && + ! wp_sizes_attribute_includes_valid_auto( $attr['sizes'] ) + ) { + $attr['sizes'] = 'auto, ' . $attr['sizes']; + } + /** * Filters the list of attachment image attributes. * @@ -1917,6 +1926,9 @@ function wp_filter_content_tags( $content, $context = null ) { // Add loading optimization attributes if applicable. $filtered_image = wp_img_tag_add_loading_optimization_attrs( $filtered_image, $context ); + // Adds auto to the sizes attribute if applicable. + $filtered_image = wp_img_tag_add_auto_sizes( $filtered_image ); + /** * Filters an img tag within the content for a given context. * @@ -1963,6 +1975,63 @@ function wp_filter_content_tags( $content, $context = null ) { return $content; } +/** + * Adds auto to the sizes attribute to the image, if image lazy loaded. + * + * @since 6.7.0 + * + * @param string|mixed $html The HTML image tag markup being filtered. + * @return string The filtered HTML image tag markup. + */ +function wp_img_tag_add_auto_sizes( $image ) { + if ( ! is_string( $image ) ) { + $image = ''; + } + + $processor = new WP_HTML_Tag_Processor( $image ); + + // Bail if there is no IMG tag. + if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) { + return $image; + } + + // Bail early if the image is not lazy-loaded. + $value = $processor->get_attribute( 'loading' ); + if ( ! is_string( $value ) || 'lazy' !== strtolower( trim( $value, " \t\f\r\n" ) ) ) { + return $image; + } + + $sizes = $processor->get_attribute( 'sizes' ); + + // Bail early if the image is not responsive. + if ( ! is_string( $sizes ) ) { + return $image; + } + + // Don't add 'auto' to the sizes attribute if it already exists. + if ( wp_sizes_attribute_includes_valid_auto( $sizes ) ) { + return $image; + } + + $processor->set_attribute( 'sizes', "auto, $sizes" ); + return $processor->get_updated_html(); +} + +/** + * Checks whether the given 'sizes' attribute includes the 'auto' keyword as the first item in the list. + * + * Per the HTML spec, if present it must be the first entry. + * + * @since 6.7.0 + * + * @param string $sizes_attr The 'sizes' attribute value. + * @return bool True if the 'auto' keyword is present, false otherwise. + */ +function wp_sizes_attribute_includes_valid_auto( string $sizes_attr ) { + $token = strtok( strtolower( $sizes_attr ), ',' ); + return false !== $token && 'auto' === trim( $token, " \t\f\r\n" ); +} + /** * Adds optimization attributes to an `img` HTML tag. * From 297cfd2fc6ea4ebc79c68645d6ab1a8c10a571e2 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 27 Aug 2024 15:35:51 +0530 Subject: [PATCH 2/2] Unit tests --- tests/phpunit/tests/media.php | 277 +++++++++++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index b58fc7353b5fa..c2a9053436332 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -2464,9 +2464,13 @@ public function test_wp_calculate_image_srcset_animated_gifs() { /** * @ticket 35045 * @ticket 33641 + * @ticket 61847 * @requires function imagejpeg */ public function test_wp_filter_content_tags_schemes() { + // Disable lazy loading attribute. + add_filter( 'wp_img_tag_add_loading_attr', '__return_false' ); + $image_meta = wp_get_attachment_metadata( self::$large_id ); $size_array = $this->get_image_size_array_from_meta( $image_meta, 'medium' ); @@ -2665,6 +2669,7 @@ public function test_get_image_send_to_editor_defaults_no_caption_no_rel() { * used in the output of `wp_get_attachment_image()`. * * @ticket 36246 + * @ticket 61847 * @requires function imagejpeg */ public function test_wp_get_attachment_image_should_use_wp_get_attachment_metadata() { @@ -2680,7 +2685,7 @@ public function test_wp_get_attachment_image_should_use_wp_get_attachment_metada 'src="' . $uploads_url . 'test-image-testsize-999x999.jpg" ' . 'class="attachment-testsize size-testsize" alt="" decoding="async" loading="lazy" ' . 'srcset="' . $uploads_url . 'test-image-testsize-999x999.jpg 999w, ' . $uploads_url . $basename . '-150x150.jpg 150w" ' . - 'sizes="(max-width: 999px) 100vw, 999px" />'; + 'sizes="auto, (max-width: 999px) 100vw, 999px" />'; $actual = wp_get_attachment_image( self::$large_id, 'testsize' ); @@ -5117,6 +5122,9 @@ static function ( $loading_attrs ) { } ); + // Remove sizes attribute as it is irrelevant for this test. + add_filter( 'wp_calculate_image_sizes', '__return_false' ); + // Add shortcode that prints a large image, and a block type that wraps it. add_shortcode( 'full_image', @@ -5992,6 +6000,273 @@ static function ( $loading_attrs ) { ); } + /** + * Test generated markup for an image with lazy loading gets auto-sizes. + * + * @ticket 61847 + */ + public function test_image_with_lazy_loading_has_auto_sizes() { + $this->assertStringContainsString( + 'sizes="auto, ', + wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => 'lazy' ) ), + 'Failed asserting that the sizes attribute for a lazy-loaded image includes "auto".' + ); + } + + /** + * Test generated markup for an image without lazy loading does not get auto-sizes. + * + * @ticket 61847 + */ + public function test_image_without_lazy_loading_does_not_have_auto_sizes() { + $this->assertStringContainsString( + 'sizes="(max-width: 1024px) 100vw, 1024px"', + wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => '' ) ), + 'Failed asserting that the sizes attribute for an image without lazy loading is set to the expected value.' + ); + } + + /** + * Test content filtered markup with lazy loading gets auto-sizes. + * + * @ticket 61847 + * + * @covers ::wp_img_tag_add_auto_sizes + */ + public function test_content_image_with_lazy_loading_has_auto_sizes() { + // Force lazy loading attribute. + add_filter( 'wp_img_tag_add_loading_attr', '__return_true' ); + + $this->assertStringContainsString( + 'sizes="auto, (max-width: 1024px) 100vw, 1024px"', + wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ), + 'Failed asserting that the sizes attribute for a content image with lazy loading includes "auto" with the expected sizes.' + ); + } + + /** + * Test content filtered markup without lazy loading does not get auto-sizes. + * + * @ticket 61847 + * + * @covers ::wp_img_tag_add_auto_sizes + */ + public function test_content_image_without_lazy_loading_does_not_have_auto_sizes() { + // Disable lazy loading attribute. + add_filter( 'wp_img_tag_add_loading_attr', '__return_false' ); + + $this->assertStringContainsString( + 'sizes="(max-width: 1024px) 100vw, 1024px"', + wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ), + 'Failed asserting that the sizes attribute for a content image without lazy loading is set to the expected value.' + ); + } + + /** + * Test generated markup for an image with 'auto' keyword already present in sizes does not receive it again. + * + * @ticket 61847 + * + * @covers ::wp_img_tag_add_auto_sizes + * @covers ::wp_sizes_attribute_includes_valid_auto + * + * @dataProvider data_image_with_existing_auto_sizes + * + * @param string $initial_sizes The initial sizes attribute to test. + * @param bool $expected_processed Whether the auto sizes should be processed or not. + */ + public function test_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) { + $image_tag = wp_get_attachment_image( + self::$large_id, + 'large', + false, + array( + // Force pre-existing 'sizes' attribute and lazy-loading. + 'sizes' => $initial_sizes, + 'loading' => 'lazy', + ) + ); + if ( $expected_processed ) { + $this->assertStringContainsString( + 'sizes="auto, ' . $initial_sizes . '"', + $image_tag, + 'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.' + ); + } else { + $this->assertStringContainsString( + 'sizes="' . $initial_sizes . '"', + $image_tag, + 'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.' + ); + } + } + + /** + * Test content filtered markup with 'auto' keyword already present in sizes does not receive it again. + * + * @ticket 61847 + * + * @covers ::wp_img_tag_add_auto_sizes + * @covers ::wp_sizes_attribute_includes_valid_auto + * + * @dataProvider data_image_with_existing_auto_sizes + * + * @param string $initial_sizes The initial sizes attribute to test. + * @param bool $expected_processed Whether the auto sizes should be processed or not. + */ + public function test_content_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) { + // Force lazy loading attribute. + add_filter( 'wp_img_tag_add_loading_attr', '__return_true' ); + + add_filter( + 'get_image_tag', + static function ( $html ) use ( $initial_sizes ) { + return str_replace( + '" />', + '" sizes="' . $initial_sizes . '" />', + $html + ); + } + ); + + $image_content = wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ); + if ( $expected_processed ) { + $this->assertStringContainsString( + 'sizes="auto, ' . $initial_sizes . '"', + $image_content, + 'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.' + ); + } else { + $this->assertStringContainsString( + 'sizes="' . $initial_sizes . '"', + $image_content, + 'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.' + ); + } + } + + /** + * Returns data for the above test methods to assert correct behavior with a pre-existing sizes attribute. + * + * @return array Arguments for the test scenarios. + */ + public function data_image_with_existing_auto_sizes() { + return array( + 'not present' => array( + '(max-width: 1024px) 100vw, 1024px', + true, + ), + 'in beginning, without space' => array( + 'auto,(max-width: 1024px) 100vw, 1024px', + false, + ), + 'in beginning, with space' => array( + 'auto, (max-width: 1024px) 100vw, 1024px', + false, + ), + 'sole keyword' => array( + 'auto', + false, + ), + 'with space before' => array( + ' auto, (max-width: 1024px) 100vw, 1024px', + false, + ), + 'with uppercase' => array( + 'AUTO, (max-width: 1024px) 100vw, 1024px', + false, + ), + + /* + * The following scenarios technically include the 'auto' keyword, + * but it is in the wrong place, as per the HTML spec it must be + * the first entry in the list. + * Therefore in these invalid cases the 'auto' keyword should still + * be added to the beginning of the list. + */ + 'within, without space' => array( + '(max-width: 1024px) 100vw, auto,1024px', + true, + ), + 'within, with space' => array( + '(max-width: 1024px) 100vw, auto, 1024px', + true, + ), + 'at the end, without space' => array( + '(max-width: 1024px) 100vw,auto', + true, + ), + 'at the end, with space' => array( + '(max-width: 1024px) 100vw, auto', + true, + ), + ); + } + + /** + * Data provider for test_auto_sizes_update_content_img_tag(). + * + * @return array + */ + public function data_provider_to_test_wp_img_tag_add_auto_sizes() { + return array( + 'expected_with_single_quoted_attributes' => array( + 'input' => "", + 'expected' => "", + ), + 'expected_with_data_sizes_attribute' => array( + 'input' => '', + 'expected' => '', + ), + 'expected_with_data_sizes_attribute_already_present' => array( + 'input' => '', + 'expected' => '', + ), + 'not_expected_with_loading_lazy_in_attr_value' => array( + 'input' => '\'This', + 'expected' => '\'This', + ), + 'not_expected_with_data_loading_attribute_present' => array( + 'input' => '', + 'expected' => '', + ), + 'expected_when_attributes_have_spaces_after_them' => array( + 'input' => '', + 'expected' => '', + ), + 'expected_when_attributes_are_upper_case' => array( + 'input' => '', + 'expected' => '', + ), + 'expected_when_loading_lazy_lacks_quotes' => array( + 'input' => '', + 'expected' => '', + ), + 'expected_when_loading_lazy_has_whitespace' => array( + 'input' => '', + 'expected' => '', + ), + 'not_expected_when_sizes_auto_lacks_quotes' => array( + 'input' => '', + 'expected' => '', + ), + ); + } + + /** + * @ticket 61847 + * + * @covers ::wp_img_tag_add_auto_sizes + * + * @dataProvider data_provider_to_test_wp_img_tag_add_auto_sizes + */ + public function test_auto_sizes_update_content_img_tag( string $input, string $expected ) { + $this->assertSame( + $expected, + wp_img_tag_add_auto_sizes( $input ) + ); + } + /** * Helper method to keep track of the last context returned by the 'wp_get_attachment_image_context' filter. *