diff --git a/plugins/auto-sizes/auto-sizes.php b/plugins/auto-sizes/auto-sizes.php index 6abd2300c1..d44f8755a9 100644 --- a/plugins/auto-sizes/auto-sizes.php +++ b/plugins/auto-sizes/auto-sizes.php @@ -28,5 +28,3 @@ define( 'IMAGE_AUTO_SIZES_VERSION', '1.2.0' ); require_once __DIR__ . '/hooks.php'; - -require_once __DIR__ . '/optimization-detective.php'; diff --git a/plugins/auto-sizes/hooks.php b/plugins/auto-sizes/hooks.php index 66d4d1efd7..4d2479b745 100644 --- a/plugins/auto-sizes/hooks.php +++ b/plugins/auto-sizes/hooks.php @@ -98,8 +98,8 @@ function auto_sizes_update_content_img_tag( $html ): string { * @return bool True if the 'auto' keyword is present, false otherwise. */ function auto_sizes_attribute_includes_valid_auto( string $sizes_attr ): bool { - $token = strtok( strtolower( $sizes_attr ), ',' ); - return false !== $token && 'auto' === trim( $token, " \t\f\r\n" ); + list( $first_size ) = explode( ',', $sizes_attr, 2 ); + return 'auto' === strtolower( trim( $first_size, " \t\f\r\n" ) ); } /** diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php deleted file mode 100644 index 3b10027dac..0000000000 --- a/plugins/auto-sizes/optimization-detective.php +++ /dev/null @@ -1,66 +0,0 @@ -processor->get_tag() ) { - return false; - } - - $sizes = $context->processor->get_attribute( 'sizes' ); - if ( ! is_string( $sizes ) ) { - return false; - } - - $sizes = preg_split( '/\s*,\s*/', $sizes ); - if ( ! is_array( $sizes ) ) { - return false; - } - - $is_lazy_loaded = ( 'lazy' === $context->processor->get_attribute( 'loading' ) ); - $has_auto_sizes = in_array( 'auto', $sizes, true ); - - $changed = false; - if ( $is_lazy_loaded && ! $has_auto_sizes ) { - array_unshift( $sizes, 'auto' ); - $changed = true; - } elseif ( ! $is_lazy_loaded && $has_auto_sizes ) { - $sizes = array_diff( $sizes, array( 'auto' ) ); - $changed = true; - } - if ( $changed ) { - $context->processor->set_attribute( 'sizes', join( ', ', $sizes ) ); - } - - return false; // Since this tag visitor does not require this tag to be included in the URL Metrics. -} - -/** - * Registers the tag visitor for image tags. - * - * @since 1.1.0 - * - * @param OD_Tag_Visitor_Registry $registry Tag visitor registry. - */ -function auto_sizes_register_tag_visitors( OD_Tag_Visitor_Registry $registry ): void { - $registry->register( 'auto-sizes', 'auto_sizes_visit_tag' ); -} - -// Important: The Image Prioritizer's IMG tag visitor is registered at priority 10, so priority 100 ensures that the loading attribute has been correctly set by the time the Auto Sizes visitor runs. -add_action( 'od_register_tag_visitors', 'auto_sizes_register_tag_visitors', 100 ); diff --git a/plugins/auto-sizes/tests/test-auto-sizes.php b/plugins/auto-sizes/tests/test-auto-sizes.php index 13898a4ae9..8a25f2b059 100644 --- a/plugins/auto-sizes/tests/test-auto-sizes.php +++ b/plugins/auto-sizes/tests/test-auto-sizes.php @@ -262,6 +262,10 @@ public function data_provider_to_test_auto_sizes_update_content_img_tag(): array 'input' => '', 'expected' => '', ), + 'expected_when_auto_size_preceded_by_extra_commas' => array( + 'input' => '', + 'expected' => '', + ), ); } diff --git a/plugins/auto-sizes/tests/test-optimization-detective.php b/plugins/auto-sizes/tests/test-optimization-detective.php deleted file mode 100644 index 0dd936c812..0000000000 --- a/plugins/auto-sizes/tests/test-optimization-detective.php +++ /dev/null @@ -1,123 +0,0 @@ -markTestSkipped( 'Optimization Detective is not active.' ); - } - } - - /** - * Tests auto_sizes_register_tag_visitors(). - * - * @covers ::auto_sizes_register_tag_visitors - */ - public function test_auto_sizes_register_tag_visitors(): void { - if ( ! class_exists( OD_Tag_Visitor_Registry::class ) ) { - $this->markTestSkipped( 'Optimization Detective is not active.' ); - } - $registry = new OD_Tag_Visitor_Registry(); - auto_sizes_register_tag_visitors( $registry ); - $this->assertTrue( $registry->is_registered( 'auto-sizes' ) ); - $this->assertEquals( 'auto_sizes_visit_tag', $registry->get_registered( 'auto-sizes' ) ); - } - - /** - * Data provider. - * - * @return array 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' => 'Foo', - 'expected' => 'Foo', - ), - - 'non_responsive_image' => array( - 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, - ), - 'buffer' => 'Quux', - 'expected' => 'Quux', - ), - - 'auto_sizes_added' => array( - 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, - ), - 'buffer' => 'Foo', - 'expected' => 'Foo', - ), - - 'auto_sizes_already_added' => array( - 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, - ), - 'buffer' => 'Foo', - 'expected' => 'Foo', - ), - - // 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' => 'Foo', - 'expected' => 'Foo', - ), - ); - } - - /** - * 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.

Baz Qux - Quux + Quux ', @@ -73,7 +73,7 @@

Now the following image is definitely outside the initial viewport.

Baz Qux - Quux + Quux ', diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints.php b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints.php new file mode 100644 index 0000000000..c51a47dcac --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints.php @@ -0,0 +1,77 @@ + static function ( Test_Image_Prioritizer_Helper $test_case ): void { + $breakpoint_max_widths = array( 480, 600, 782 ); + + add_filter( + 'od_breakpoint_max_widths', + static function () use ( $breakpoint_max_widths ) { + return $breakpoint_max_widths; + } + ); + + foreach ( array_merge( $breakpoint_max_widths, array( 1600 ) ) as $i => $viewport_width ) { + $elements = array( + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[4][self::IMG]', + ), + ); + $elements[ $i ]['isLCP'] = true; + OD_URL_Metrics_Post_Type::store_url_metric( + od_get_url_metrics_slug( od_get_normalized_query_vars() ), + $test_case->get_sample_url_metric( + array( + 'viewport_width' => $viewport_width, + 'elements' => $elements, + ) + ) + ); + } + }, + 'buffer' => ' + + + + ... + + + Mobile Logo + Phablet Logo + Tablet Logo + Desktop Logo + + + ', + 'expected' => ' + + + + ... + + + + + + + Mobile Logo + Phablet Logo + Tablet Logo + Desktop Logo + + + + ', +); diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index 716e057bc6..708c062a53 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -63,4 +63,100 @@ public function test_image_prioritizer_register_tag_visitors( Closure $set_up, s "Buffer snapshot:\n$buffer" ); } + + /** + * Data provider. + * + * @return array Data. + */ + public function data_provider_test_auto_sizes(): 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' => 'Foo', + 'expected' => 'Foo', + ), + + 'non_responsive_image' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Quux', + 'expected' => 'Quux', + ), + + 'auto_sizes_added' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + + 'auto_sizes_already_added' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + + // 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' => 'Foo', + 'expected' => 'Foo', + ), + + 'wrongly_auto_sized_responsive_img_with_only_auto' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 1, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + ); + } + + /** + * Test auto sizes. + * + * @covers Image_Prioritizer_Img_Tag_Visitor::__invoke + * + * @dataProvider data_provider_test_auto_sizes + * @phpstan-param array{ xpath: string, isLCP: bool, intersectionRatio: int } $element_metrics + */ + public function test_auto_sizes( 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/optimization-detective/class-od-link-collection.php b/plugins/optimization-detective/class-od-link-collection.php index 28084c9bd5..b1fbfacb6c 100644 --- a/plugins/optimization-detective/class-od-link-collection.php +++ b/plugins/optimization-detective/class-od-link-collection.php @@ -25,7 +25,7 @@ * href?: non-empty-string, * imagesrcset?: non-empty-string, * imagesizes?: non-empty-string, - * crossorigin?: ''|'anonymous'|'use-credentials', + * crossorigin?: 'anonymous'|'use-credentials', * fetchpriority?: 'high'|'low'|'auto', * as?: 'audio'|'document'|'embed'|'fetch'|'font'|'image'|'object'|'script'|'style'|'track'|'video'|'worker', * media?: non-empty-string,