Make WordPress Core

Changeset 59008


Ignore:
Timestamp:
09/10/2024 11:59:54 PM (3 months ago)
Author:
joemcgill
Message:

Media: Add auto sizes for lazy-loaded images.

This implements the HTML spec for applying auto sizes to lazy-loaded images by prepending auto to the sizes attribute generated by WordPress if the image has a loading attribute set to lazy. For browser that support this HTML spec, the image's size value will be set to the concrete object size of the image. For browsers that don't support the spec, the word "auto" will be ignored when parsing the sizes value.

References:

Props mukesh27, flixos90, joemcgill, westonruter, peterwilsoncc.
Fixes #61847.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/media.php

    r58974 r59008  
    11381138        }
    11391139
     1140        // Adds 'auto' to the sizes attribute if applicable.
     1141        if (
     1142            isset( $attr['loading'] ) &&
     1143            'lazy' === $attr['loading'] &&
     1144            isset( $attr['sizes'] ) &&
     1145            ! wp_sizes_attribute_includes_valid_auto( $attr['sizes'] )
     1146        ) {
     1147            $attr['sizes'] = 'auto, ' . $attr['sizes'];
     1148        }
     1149
    11401150        /**
    11411151         * Filters the list of attachment image attributes.
     
    19181928            $filtered_image = wp_img_tag_add_loading_optimization_attrs( $filtered_image, $context );
    19191929
     1930            // Adds 'auto' to the sizes attribute if applicable.
     1931            $filtered_image = wp_img_tag_add_auto_sizes( $filtered_image );
     1932
    19201933            /**
    19211934             * Filters an img tag within the content for a given context.
     
    19621975
    19631976    return $content;
     1977}
     1978
     1979/**
     1980 * Adds 'auto' to the sizes attribute to the image, if the image is lazy loaded and does not already include it.
     1981 *
     1982 * @since 6.7.0
     1983 *
     1984 * @param string $image The image tag markup being filtered.
     1985 * @return string The filtered image tag markup.
     1986 */
     1987function wp_img_tag_add_auto_sizes( string $image ): string {
     1988    $processor = new WP_HTML_Tag_Processor( $image );
     1989
     1990    // Bail if there is no IMG tag.
     1991    if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
     1992        return $image;
     1993    }
     1994
     1995    // Bail early if the image is not lazy-loaded.
     1996    $value = $processor->get_attribute( 'loading' );
     1997    if ( ! is_string( $value ) || 'lazy' !== strtolower( trim( $value, " \t\f\r\n" ) ) ) {
     1998        return $image;
     1999    }
     2000
     2001    $sizes = $processor->get_attribute( 'sizes' );
     2002
     2003    // Bail early if the image is not responsive.
     2004    if ( ! is_string( $sizes ) ) {
     2005        return $image;
     2006    }
     2007
     2008    // Don't add 'auto' to the sizes attribute if it already exists.
     2009    if ( wp_sizes_attribute_includes_valid_auto( $sizes ) ) {
     2010        return $image;
     2011    }
     2012
     2013    $processor->set_attribute( 'sizes', "auto, $sizes" );
     2014    return $processor->get_updated_html();
     2015}
     2016
     2017/**
     2018 * Checks whether the given 'sizes' attribute includes the 'auto' keyword as the first item in the list.
     2019 *
     2020 * Per the HTML spec, if present it must be the first entry.
     2021 *
     2022 * @since 6.7.0
     2023 *
     2024 * @param string $sizes_attr The 'sizes' attribute value.
     2025 * @return bool True if the 'auto' keyword is present, false otherwise.
     2026 */
     2027function wp_sizes_attribute_includes_valid_auto( string $sizes_attr ): bool {
     2028    list( $first_size ) = explode( ',', $sizes_attr, 2 );
     2029    return 'auto' === strtolower( trim( $first_size, " \t\f\r\n" ) );
    19642030}
    19652031
  • trunk/tests/phpunit/tests/media.php

    r58974 r59008  
    24682468     */
    24692469    public function test_wp_filter_content_tags_schemes() {
     2470        // Disable lazy loading attribute to not add the 'auto' keyword to the `sizes` attribute.
     2471        add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
     2472
    24702473        $image_meta = wp_get_attachment_metadata( self::$large_id );
    24712474        $size_array = $this->get_image_size_array_from_meta( $image_meta, 'medium' );
     
    26812684            'class="attachment-testsize size-testsize" alt="" decoding="async" loading="lazy" ' .
    26822685            'srcset="' . $uploads_url . 'test-image-testsize-999x999.jpg 999w, ' . $uploads_url . $basename . '-150x150.jpg 150w" ' .
    2683             'sizes="(max-width: 999px) 100vw, 999px" />';
     2686            'sizes="auto, (max-width: 999px) 100vw, 999px" />';
    26842687
    26852688        $actual = wp_get_attachment_image( self::$large_id, 'testsize' );
     
    51175120            }
    51185121        );
     5122
     5123        // Do not calculate sizes attribute as it is irrelevant for this test.
     5124        add_filter( 'wp_calculate_image_sizes', '__return_false' );
    51195125
    51205126        // Add shortcode that prints a large image, and a block type that wraps it.
     
    60306036
    60316037    /**
     6038     * Test generated markup for an image with lazy loading gets auto-sizes.
     6039     *
     6040     * @ticket 61847
     6041     */
     6042    public function test_image_with_lazy_loading_has_auto_sizes() {
     6043        $this->assertStringContainsString(
     6044            'sizes="auto, ',
     6045            wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => 'lazy' ) ),
     6046            'Failed asserting that the sizes attribute for a lazy-loaded image includes "auto".'
     6047        );
     6048    }
     6049
     6050    /**
     6051     * Test generated markup for an image without lazy loading does not get auto-sizes.
     6052     *
     6053     * @ticket 61847
     6054     */
     6055    public function test_image_without_lazy_loading_does_not_have_auto_sizes() {
     6056        $this->assertStringNotContainsString(
     6057            'sizes="auto, ',
     6058            wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => false ) ),
     6059            'Failed asserting that the sizes attribute for an image without lazy loading does not include "auto".'
     6060        );
     6061    }
     6062
     6063    /**
     6064     * Test content filtered markup with lazy loading gets auto-sizes.
     6065     *
     6066     * @ticket 61847
     6067     *
     6068     * @covers ::wp_img_tag_add_auto_sizes
     6069     */
     6070    public function test_content_image_with_lazy_loading_has_auto_sizes() {
     6071        // Force lazy loading attribute.
     6072        add_filter( 'wp_img_tag_add_loading_attr', '__return_true' );
     6073
     6074        $this->assertStringContainsString(
     6075            'sizes="auto, (max-width: 1024px) 100vw, 1024px"',
     6076            wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ),
     6077            'Failed asserting that the sizes attribute for a content image with lazy loading includes "auto" with the expected sizes.'
     6078        );
     6079    }
     6080
     6081    /**
     6082     * Test content filtered markup without lazy loading does not get auto-sizes.
     6083     *
     6084     * @ticket 61847
     6085     *
     6086     * @covers ::wp_img_tag_add_auto_sizes
     6087     */
     6088    public function test_content_image_without_lazy_loading_does_not_have_auto_sizes() {
     6089        // Disable lazy loading attribute.
     6090        add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
     6091
     6092        $this->assertStringNotContainsString(
     6093            'sizes="auto, ',
     6094            wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ),
     6095            'Failed asserting that the sizes attribute for a content image without lazy loading does not include "auto" with the expected sizes.'
     6096        );
     6097    }
     6098
     6099    /**
     6100     * Test generated markup for an image with 'auto' keyword already present in sizes does not receive it again.
     6101     *
     6102     * @ticket 61847
     6103     *
     6104     * @covers ::wp_img_tag_add_auto_sizes
     6105     * @covers ::wp_sizes_attribute_includes_valid_auto
     6106     *
     6107     * @dataProvider data_image_with_existing_auto_sizes
     6108     *
     6109     * @param string $initial_sizes      The initial sizes attribute to test.
     6110     * @param bool   $expected_processed Whether the auto sizes should be processed or not.
     6111     */
     6112    public function test_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) {
     6113        $image_tag = wp_get_attachment_image(
     6114            self::$large_id,
     6115            'large',
     6116            false,
     6117            array(
     6118                // Force pre-existing 'sizes' attribute and lazy-loading.
     6119                'sizes'   => $initial_sizes,
     6120                'loading' => 'lazy',
     6121            )
     6122        );
     6123        if ( $expected_processed ) {
     6124            $this->assertStringContainsString(
     6125                'sizes="auto, ' . $initial_sizes . '"',
     6126                $image_tag,
     6127                'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.'
     6128            );
     6129        } else {
     6130            $this->assertStringContainsString(
     6131                'sizes="' . $initial_sizes . '"',
     6132                $image_tag,
     6133                'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.'
     6134            );
     6135        }
     6136    }
     6137
     6138    /**
     6139     * Test content filtered markup with 'auto' keyword already present in sizes does not receive it again.
     6140     *
     6141     * @ticket 61847
     6142     *
     6143     * @covers ::wp_img_tag_add_auto_sizes
     6144     * @covers ::wp_sizes_attribute_includes_valid_auto
     6145     *
     6146     * @dataProvider data_image_with_existing_auto_sizes
     6147     *
     6148     * @param string $initial_sizes      The initial sizes attribute to test.
     6149     * @param bool   $expected_processed Whether the auto sizes should be processed or not.
     6150     */
     6151    public function test_content_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) {
     6152        // Force lazy loading attribute.
     6153        add_filter( 'wp_img_tag_add_loading_attr', '__return_true' );
     6154
     6155        add_filter(
     6156            'get_image_tag',
     6157            static function ( $html ) use ( $initial_sizes ) {
     6158                return str_replace(
     6159                    '" />',
     6160                    '" sizes="' . $initial_sizes . '" />',
     6161                    $html
     6162                );
     6163            }
     6164        );
     6165
     6166        $image_content = wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) );
     6167        if ( $expected_processed ) {
     6168            $this->assertStringContainsString(
     6169                'sizes="auto, ' . $initial_sizes . '"',
     6170                $image_content,
     6171                'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.'
     6172            );
     6173        } else {
     6174            $this->assertStringContainsString(
     6175                'sizes="' . $initial_sizes . '"',
     6176                $image_content,
     6177                'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.'
     6178            );
     6179        }
     6180    }
     6181
     6182    /**
     6183     * Returns data for the above test methods to assert correct behavior with a pre-existing sizes attribute.
     6184     *
     6185     * @return array<string, mixed[]> Arguments for the test scenarios.
     6186     */
     6187    public function data_image_with_existing_auto_sizes() {
     6188        return array(
     6189            'not present'                 => array(
     6190                '(max-width: 1024px) 100vw, 1024px',
     6191                true,
     6192            ),
     6193            'in beginning, without space' => array(
     6194                'auto,(max-width: 1024px) 100vw, 1024px',
     6195                false,
     6196            ),
     6197            'in beginning, with space'    => array(
     6198                'auto, (max-width: 1024px) 100vw, 1024px',
     6199                false,
     6200            ),
     6201            'sole keyword'                => array(
     6202                'auto',
     6203                false,
     6204            ),
     6205            'with space before'           => array(
     6206                ' auto, (max-width: 1024px) 100vw, 1024px',
     6207                false,
     6208            ),
     6209            'with uppercase'              => array(
     6210                'AUTO, (max-width: 1024px) 100vw, 1024px',
     6211                false,
     6212            ),
     6213
     6214            /*
     6215             * The following scenarios technically include the 'auto' keyword,
     6216             * but it is in the wrong place, as per the HTML spec it must be
     6217             * the first entry in the list.
     6218             * Therefore in these invalid cases the 'auto' keyword should still
     6219             * be added to the beginning of the list.
     6220             */
     6221            'within, without space'       => array(
     6222                '(max-width: 1024px) 100vw, auto,1024px',
     6223                true,
     6224            ),
     6225            'within, with space'          => array(
     6226                '(max-width: 1024px) 100vw, auto, 1024px',
     6227                true,
     6228            ),
     6229            'at the end, without space'   => array(
     6230                '(max-width: 1024px) 100vw,auto',
     6231                true,
     6232            ),
     6233            'at the end, with space'      => array(
     6234                '(max-width: 1024px) 100vw, auto',
     6235                true,
     6236            ),
     6237        );
     6238    }
     6239
     6240    /**
     6241     * Data provider for test_wp_img_tag_add_auto_sizes().
     6242     *
     6243     * @return array<string, mixed>
     6244     */
     6245    public function data_provider_to_test_wp_img_tag_add_auto_sizes() {
     6246        return array(
     6247            'expected_with_single_quoted_attributes'       => array(
     6248                'input'    => "<img src='https://example.com/foo-300x225.jpg' srcset='https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w' sizes='(max-width: 650px) 100vw, 650px' loading='lazy'>",
     6249                'expected' => "<img src='https://example.com/foo-300x225.jpg' srcset='https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w' sizes=\"auto, (max-width: 650px) 100vw, 650px\" loading='lazy'>",
     6250            ),
     6251            'expected_with_data_sizes_attribute'           => array(
     6252                'input'    => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading="lazy">',
     6253                'expected' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading="lazy">',
     6254            ),
     6255            'expected_with_data_sizes_attribute_already_present' => array(
     6256                'input'    => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="AUTO, (max-width: 650px) 100vw, 650px" loading="lazy">',
     6257                'expected' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="AUTO, (max-width: 650px) 100vw, 650px" loading="lazy">',
     6258            ),
     6259            'not_expected_with_loading_lazy_in_attr_value' => array(
     6260                'input'    => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" alt=\'This is the LCP image and it should not get loading="lazy"!\'>',
     6261                'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" alt=\'This is the LCP image and it should not get loading="lazy"!\'>',
     6262            ),
     6263            'not_expected_with_data_loading_attribute_present' => array(
     6264                'input'    => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" data-removed-loading="lazy">',
     6265                'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" data-removed-loading="lazy">',
     6266            ),
     6267            'expected_when_attributes_have_spaces_after_them' => array(
     6268                'input'    => '<img src = "https://example.com/foo-300x225.jpg" srcset = "https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes = "(max-width: 650px) 100vw, 650px" loading = "lazy">',
     6269                'expected' => '<img src = "https://example.com/foo-300x225.jpg" srcset = "https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading = "lazy">',
     6270            ),
     6271            'expected_when_attributes_are_upper_case'      => array(
     6272                'input'    => '<IMG SRC="https://example.com/foo-300x225.jpg" SRCSET="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" SIZES="(max-width: 650px) 100vw, 650px" LOADING="LAZY">',
     6273                'expected' => '<IMG SRC="https://example.com/foo-300x225.jpg" SRCSET="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" LOADING="LAZY">',
     6274            ),
     6275            'expected_when_loading_lazy_lacks_quotes'      => array(
     6276                'input'    => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading=lazy>',
     6277                'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading=lazy>',
     6278            ),
     6279            'expected_when_loading_lazy_has_whitespace'    => array(
     6280                'input'    => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading=" lazy ">',
     6281                'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading=" lazy ">',
     6282            ),
     6283            'not_expected_when_sizes_auto_lacks_quotes'    => array(
     6284                'input'    => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes=auto loading="lazy">',
     6285                'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes=auto loading="lazy">',
     6286            ),
     6287        );
     6288    }
     6289
     6290    /**
     6291     * @ticket 61847
     6292     *
     6293     * @covers ::wp_img_tag_add_auto_sizes
     6294     *
     6295     * @dataProvider data_provider_to_test_wp_img_tag_add_auto_sizes
     6296     *
     6297     * @param string $input    The input HTML string.
     6298     * @param string $expected The expected output HTML string.
     6299     */
     6300    public function test_wp_img_tag_add_auto_sizes( string $input, string $expected ) {
     6301        $this->assertSame(
     6302            $expected,
     6303            wp_img_tag_add_auto_sizes( $input ),
     6304            'Failed asserting that "auto" keyword is correctly added or not added to sizes attribute in the image tag.'
     6305        );
     6306    }
     6307
     6308    /**
    60326309     * Helper method to keep track of the last context returned by the 'wp_get_attachment_image_context' filter.
    60336310     *
Note: See TracChangeset for help on using the changeset viewer.