Data.
- */
- public function data_provider_test_od_optimize_template_output_buffer(): array {
- return array(
- // Note: The Image Prioritizer plugin removes the loading attribute, and so then Auto Sizes does not then add sizes=auto.
- 'wrongly_lazy_responsive_img' => array(
- 'element_metrics' => array(
- 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
- 'isLCP' => false,
- 'intersectionRatio' => 1,
- ),
- 'buffer' => '
',
- 'expected' => '
',
- ),
-
- 'non_responsive_image' => array(
- 'element_metrics' => array(
- 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
- 'isLCP' => false,
- 'intersectionRatio' => 0,
- ),
- 'buffer' => '
',
- 'expected' => '
',
- ),
-
- 'auto_sizes_added' => array(
- 'element_metrics' => array(
- 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
- 'isLCP' => false,
- 'intersectionRatio' => 0,
- ),
- 'buffer' => '
',
- 'expected' => '
',
- ),
-
- 'auto_sizes_already_added' => array(
- 'element_metrics' => array(
- 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
- 'isLCP' => false,
- 'intersectionRatio' => 0,
- ),
- 'buffer' => '
',
- 'expected' => '
',
- ),
-
- // If Auto Sizes added the sizes=auto attribute but Image Prioritizer ended up removing it due to the image not being lazy-loaded, remove sizes=auto again.
- 'wrongly_auto_sized_responsive_img' => array(
- 'element_metrics' => array(
- 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
- 'isLCP' => false,
- 'intersectionRatio' => 1,
- ),
- 'buffer' => '
',
- 'expected' => '
',
- ),
- );
- }
-
- /**
- * Test auto_sizes_visit_tag().
- *
- * @covers ::auto_sizes_visit_tag
- *
- * @dataProvider data_provider_test_od_optimize_template_output_buffer
- * @phpstan-param array{ xpath: string, isLCP: bool, intersectionRatio: int } $element_metrics
- */
- public function test_od_optimize_template_output_buffer( array $element_metrics, string $buffer, string $expected ): void {
- $this->populate_url_metrics( array( $element_metrics ) );
-
- $html_start_doc = '...';
- $html_end_doc = '';
-
- $buffer = od_optimize_template_output_buffer( $html_start_doc . $buffer . $html_end_doc );
- $buffer = preg_replace( '#.+?]*>#s', '', $buffer );
- $buffer = preg_replace( '#.*$#s', '', $buffer );
-
- $this->assertEquals(
- $this->remove_initial_tabs( $expected ),
- $this->remove_initial_tabs( $buffer ),
- "Buffer snapshot:\n$buffer"
- );
- }
-}
diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
index 287e955f31..43151939b3 100644
--- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
+++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
@@ -40,6 +40,20 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
$xpath = $processor->get_xpath();
+ /**
+ * Gets attribute value.
+ *
+ * @param string $attribute_name Attribute name.
+ * @return string|true|null Normalized attribute value.
+ */
+ $get_attribute_value = static function ( string $attribute_name ) use ( $processor ) {
+ $value = $processor->get_attribute( $attribute_name );
+ if ( is_string( $value ) ) {
+ $value = strtolower( trim( $value, " \t\f\r\n" ) );
+ }
+ return $value;
+ };
+
/*
* When the same LCP element is common/shared among all viewport groups, make sure that the element has
* fetchpriority=high, even though it won't really be needed because a preload link with fetchpriority=high
@@ -47,7 +61,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
*/
$common_lcp_element = $context->url_metrics_group_collection->get_common_lcp_element();
if ( ! is_null( $common_lcp_element ) && $xpath === $common_lcp_element['xpath'] ) {
- if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
+ if ( 'high' === $get_attribute_value( 'fetchpriority' ) ) {
$processor->set_meta_attribute( 'fetchpriority-already-added', true );
} else {
$processor->set_attribute( 'fetchpriority', 'high' );
@@ -81,7 +95,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
} else {
// Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy.
$is_visible = $element_max_intersection_ratio > 0.0;
- $loading = (string) $processor->get_attribute( 'loading' );
+ $loading = $get_attribute_value( 'loading' );
if ( $is_visible && 'lazy' === $loading ) {
$processor->remove_attribute( 'loading' );
} elseif ( ! $is_visible && 'lazy' !== $loading ) {
@@ -90,6 +104,23 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
}
// TODO: If an image is visible in one breakpoint but not another, add loading=lazy AND add a regular-priority preload link with media queries (unless LCP in which case it should already have a fetchpriority=high link) so that the image won't be eagerly-loaded for viewports on which it is not shown.
+ // Ensure that sizes=auto is set properly.
+ $sizes = $processor->get_attribute( 'sizes' );
+ if ( is_string( $sizes ) ) {
+ $is_lazy = 'lazy' === $get_attribute_value( 'loading' );
+ $has_auto = $this->sizes_attribute_includes_valid_auto( $sizes );
+
+ if ( $is_lazy && ! $has_auto ) {
+ $processor->set_attribute( 'sizes', "auto, $sizes" );
+ } elseif ( ! $is_lazy && $has_auto ) {
+ // Remove auto from the beginning of the list.
+ $processor->set_attribute(
+ 'sizes',
+ (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes )
+ );
+ }
+ }
+
// If this element is the LCP (for a breakpoint group), add a preload link for it.
foreach ( $context->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) {
$link_attributes = array_merge(
@@ -110,8 +141,8 @@ static function ( string $value ): bool {
)
);
- $crossorigin = $processor->get_attribute( 'crossorigin' );
- if ( is_string( $crossorigin ) ) {
+ $crossorigin = $get_attribute_value( 'crossorigin' );
+ if ( null !== $crossorigin ) {
$link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous';
}
@@ -126,4 +157,24 @@ static function ( string $value ): bool {
return true;
}
+
+ /**
+ * 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 n.e.x.t
+ *
+ * @param string $sizes_attr The 'sizes' attribute value.
+ * @return bool True if the 'auto' keyword is present, false otherwise.
+ */
+ private function sizes_attribute_includes_valid_auto( string $sizes_attr ): bool {
+ if ( function_exists( 'wp_sizes_attribute_includes_valid_auto' ) ) {
+ return wp_sizes_attribute_includes_valid_auto( $sizes_attr );
+ } elseif ( function_exists( 'auto_sizes_attribute_includes_valid_auto' ) ) {
+ return auto_sizes_attribute_includes_valid_auto( $sizes_attr );
+ } else {
+ return 'auto' === $sizes_attr || str_starts_with( $sizes_attr, 'auto,' );
+ }
+ }
}
diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php
index 356ddfb0d6..31f6a9eba6 100644
--- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php
+++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php
@@ -55,7 +55,7 @@
Now the following image is definitely outside the initial viewport.
-
+