Skip to content

Injection of sizes=auto into images can be more robust with HTML Tag Processor #1446

@westonruter

Description

@westonruter

As discussed in #1445, there is an opportunity to leverage the HTML Tag Processor to make auto_sizes_update_content_img_tag() more robust as follows:

function auto_sizes_update_content_img_tag( $html ): string {
	if ( ! is_string( $html ) ) {
		$html = '';
	}

	$processor = new WP_HTML_Tag_Processor( $html );

	// Bail if there is no IMG tag.
	if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
		return $html;
	}

	// Bail early if the image is not lazy-loaded.
	if ( 'lazy' !== $processor->get_attribute( 'loading' ) ) {
		return $html;
	}

	$sizes = $processor->get_attribute( 'sizes' );

	// Bail early if the image is not responsive.
	if ( ! is_string( $sizes ) ) {
		return $html;
	}

	// Don't add 'auto' to the sizes attribute if it already exists.
	if ( auto_sizes_attribute_includes_auto( $sizes ) ) {
		return $html;
	}

	$processor->set_attribute( 'sizes', "auto, $sizes" );
	return $processor->get_updated_html();
}

This not only provides a better DX, but it is safer and ensures the result is correct. It's being used increasingly throughout core as this is its intended purpose. We're also using it in our Performance plugins:

function auto_sizes_filter_image_tag( string $content, array $parsed_block ): string {
$processor = new WP_HTML_Tag_Processor( $content );
$has_image = $processor->next_tag( array( 'tag_name' => 'img' ) );
// Only update the markup if an image is found.
if ( $has_image ) {
$processor->set_attribute( 'data-needs-sizes-update', true );
if ( isset( $parsed_block['attrs']['align'] ) ) {
$processor->set_attribute( 'data-align', $parsed_block['attrs']['align'] );
}
// Resize image width.
if ( isset( $parsed_block['attrs']['width'] ) ) {
$processor->set_attribute( 'data-resize-width', $parsed_block['attrs']['width'] );
}
$content = $processor->get_updated_html();
}
return $content;
}

function auto_sizes_improve_image_sizes_attributes( string $content ): string {
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( array( 'tag_name' => 'img' ) ) ) {
return $content;
}

$processor = new WP_HTML_Tag_Processor( $image );
if ( $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
$width = (int) $processor->get_attribute( 'width' );
$height = (int) $processor->get_attribute( 'height' );
}
$size_to_use = ( $width > 0 && $height > 0 ) ? array( $width, $height ) : 'full';

foreach ( $img_tags as list( $img ) ) {
$processor = new WP_HTML_Tag_Processor( $img );
if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
// This condition won't ever be met since we're iterating over the IMG tags extracted with preg_match_all() above.
continue;
}
// Find the ID of each image by the class.
// TODO: It would be preferable to use the $processor->class_list() method but there seems to be some typing issues with PHPStan.
$class_name = $processor->get_attribute( 'class' );
if (
! is_string( $class_name )
||
1 !== preg_match( '/(?:^|\s)wp-image-([1-9]\d*)(?:\s|$)/i', $class_name, $matches )
) {
continue;
}
// Make sure we use the last item on the list of matches.
$images[ $img ] = (int) $matches[1];
}

Test Cases

Here are some cases where the current implementation breaks. Some of them rely on attribute values being single-quoted, and it's true that core only emits double quotes for attribute values. However, images can come from other places than core. For example, these snippets can be pasted in a Custom HTML block to reproduce the problems. Problematic attribute modifications could also come from plugin filters. Additionally, images may come from Embed blocks. (Aside: I just discovered that if I have these four images in Custom HTML blocks and then put a Flickr embed as the 5th block, core's heuristics are incorrectly adding fetchpriority=high to this image from Flickr since it apparently doesn't count the images in the Custom HTML blocks.)

Incorrectly not adding auto when it should be present due to the use of single quotes:

<img
    src='https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg'
    srcset='https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg 300w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1024x768.jpg 1024w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-768x576.jpg 768w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1536x1152.jpg 1536w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-2048x1536.jpg 2048w'
    sizes='(max-width: 650px) 100vw, 650px'
    loading='lazy'
>

Incorrectly prefixing a data-tshirt-sizes attribute with auto and duplicating auto on an existing sizes attribute:

<img
    data-tshirt-sizes="S M L"
    src="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg"
    srcset="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg 300w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1024x768.jpg 1024w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-768x576.jpg 768w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1536x1152.jpg 1536w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-2048x1536.jpg 2048w"
    sizes="auto, (max-width: 650px) 100vw, 650px"
    loading="lazy"
>

Incorrectly adding auto due to loading="lazy" appearing in an attribute value:

<img
    src="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg"
    srcset="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg 300w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1024x768.jpg 1024w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-768x576.jpg 768w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1536x1152.jpg 1536w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-2048x1536.jpg 2048w"
    sizes="(max-width: 650px) 100vw, 650px"
    alt='This is the LCP image and it should not get loading="lazy"!'
>

Incorrectly adding auto for an eager-loaded image when there is a data-removed-loading="lazy" attribute:

<img
    src="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg"
    srcset="https://pd.w.org/2024/08/36566abfcb2952e20.99062056-300x225.jpg 300w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1024x768.jpg 1024w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-768x576.jpg 768w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-1536x1152.jpg 1536w, https://pd.w.org/2024/08/36566abfcb2952e20.99062056-2048x1536.jpg 2048w"
    sizes="(max-width: 650px) 100vw, 650px"
    data-removed-loading="lazy"
>

All of these cases are fixed by using the HTML Tag Processor.

Other scenarios from @dmsnell in #1445 (comment):

  • the sizes attribute could be in any case, but this only tests for all-lowercase matches
  • the sizes attribute could have single quotes, or no quotes, and have whitespace around the equals sign, e.g. Sizes = Auto
  • the sizes attribute must be preceded by whitespace or a /, but this test also matches data-sizes="unrelated attribute"

Metadata

Metadata

Labels

[Plugin] Enhanced Responsive ImagesIssues for the Enhanced Responsive Images plugin (formerly Auto Sizes)[Type] EnhancementA suggestion for improvement of an existing feature

Type

No type

Projects

Status

Done 😃

Relationships

None yet

Development

No branches or pull requests

Issue actions