From 7fdc1810eb4a0b06593572e141809034d8ef95b3 Mon Sep 17 00:00:00 2001 From: AhmarZaidi Date: Sat, 29 Jun 2024 05:15:45 +0530 Subject: [PATCH 1/8] Add support for sending preload links via HTTP headers --- .../class-od-preload-link-collection.php | 31 +++++++++++++++++++ .../optimization-detective/optimization.php | 5 +++ 2 files changed, 36 insertions(+) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index 6c0acdb8da..bd779a1b0c 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -166,6 +166,37 @@ public function get_html(): string { return implode( '', $link_tags ); } + /** + * Gets the HTTP Link header string. + * + * @return string HTTP Link header. + */ + public function get_headers(): string { + $link_headers = array(); + + foreach ( $this->get_adjacent_deduplicated_links() as $link ) { + $media_features = array( 'screen' ); + if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { + $media_features[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); + } + if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { + $media_features[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); + } + $link['attributes']['media'] = implode( ' and ', $media_features ); + + $link_header = '<' . esc_url( $link['attributes']['href'] ?? '' ) . '>; rel="preload"'; + foreach ( $link['attributes'] as $name => $value ) { + if ( 'href' !== $name ) { + $link_header .= sprintf( '; %s="%s"', $name, esc_attr( $value ) ); + } + } + + $link_headers[] = $link_header; + } + + return 'Link: ' . implode( ', ', $link_headers ); + } + /** * Counts the links. * diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index e3a9696c85..61a1f49522 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -164,6 +164,11 @@ function od_optimize_template_output_buffer( string $buffer ): string { $tag_visitor_registry = new OD_Tag_Visitor_Registry(); + // Send any preload links as Link headers. + if ( count( $preload_links ) > 0 && ! headers_sent() ) { + header( $preload_links->get_headers() ); + } + /** * Fires to register tag visitors before walking over the document to perform optimizations. * From 1e3a242c4780a5c45683580fd0dbd254d014d68c Mon Sep 17 00:00:00 2001 From: AhmarZaidi Date: Tue, 2 Jul 2024 16:46:21 +0530 Subject: [PATCH 2/8] PR Changes - Remove code duplication - Update escaping functions - Update documentation - Handle no headers case --- .../class-od-preload-link-collection.php | 52 +++++++++++-------- .../optimization-detective/optimization.php | 11 ++-- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index bd779a1b0c..618c019ba1 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -136,6 +136,27 @@ static function ( array $carry, array $link ): array { ); } + /** + * Adds media features to the links. + * + * @phpstan-param Link $link + * @param array $link Link array. + * @phpstan-return Link + * @return array Link array with media features added. + */ + private function add_media_features( array $link ): array { + $media_features = array( 'screen' ); + if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { + $media_features[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); + } + if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { + $media_features[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); + } + $link['attributes']['media'] = implode( ' and ', $media_features ); + + return (array) $link; + } + /** * Gets the HTML for the link tags. * @@ -145,14 +166,7 @@ public function get_html(): string { $link_tags = array(); foreach ( $this->get_adjacent_deduplicated_links() as $link ) { - $media_features = array( 'screen' ); - if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { - $media_features[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); - } - if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { - $media_features[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); - } - $link['attributes']['media'] = implode( ' and ', $media_features ); + $link = $this->add_media_features( (array) $link ); $link_tag = ' $value ) { @@ -167,32 +181,28 @@ public function get_html(): string { } /** - * Gets the HTTP Link header string. + * Constructs the Link HTTP response header. * - * @return string HTTP Link header. + * @return string|null Link HTTP response header, or null if there are none. */ - public function get_headers(): string { + public function get_response_header(): ?string { $link_headers = array(); foreach ( $this->get_adjacent_deduplicated_links() as $link ) { - $media_features = array( 'screen' ); - if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { - $media_features[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); - } - if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { - $media_features[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); - } - $link['attributes']['media'] = implode( ' and ', $media_features ); + $link = $this->add_media_features( (array) $link ); - $link_header = '<' . esc_url( $link['attributes']['href'] ?? '' ) . '>; rel="preload"'; + $link_header = '<' . esc_url_raw( $link['attributes']['href'] ?? '' ) . '>; rel="preload"'; foreach ( $link['attributes'] as $name => $value ) { if ( 'href' !== $name ) { - $link_header .= sprintf( '; %s="%s"', $name, esc_attr( $value ) ); + $link_header .= sprintf( '; %s="%s"', $name, rawurlencode( $value ) ); } } $link_headers[] = $link_header; } + if ( count( $link_headers ) === 0 ) { + return null; + } return 'Link: ' . implode( ', ', $link_headers ); } diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index 61a1f49522..2ae83ba295 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -164,11 +164,6 @@ function od_optimize_template_output_buffer( string $buffer ): string { $tag_visitor_registry = new OD_Tag_Visitor_Registry(); - // Send any preload links as Link headers. - if ( count( $preload_links ) > 0 && ! headers_sent() ) { - header( $preload_links->get_headers() ); - } - /** * Fires to register tag visitors before walking over the document to perform optimizations. * @@ -194,8 +189,12 @@ function od_optimize_template_output_buffer( string $buffer ): string { $generator->next(); } - // Inject any preload links at the end of the HEAD. + // Send any preload links in a Link response header and in a LINK tag injected at the end of the HEAD. if ( count( $preload_links ) > 0 ) { + $response_header_links = $preload_links->get_response_header(); + if ( ! is_null( $response_header_links ) && ! headers_sent() ) { + header( $response_header_links, false ); + } $walker->append_head_html( $preload_links->get_html() ); } From b36959a34da3c9fe8e229f6957f643d43a3cabad Mon Sep 17 00:00:00 2001 From: AhmarZaidi Date: Wed, 3 Jul 2024 14:04:26 +0530 Subject: [PATCH 3/8] Prepare links - Deduplicate adjacent links - Add media attributes --- .../class-od-preload-link-collection.php | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index 618c019ba1..a7c92d02e0 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -76,14 +76,14 @@ public function add_link( array $attributes, ?int $minimum_viewport_width, ?int } /** - * Get adjacent-deduplicated links. + * Prepare links by deduplicating adjacent links and adding media attributes. * * When two links are identical except for their minimum/maximum widths which are also consecutive, then merge them - * together. + * together. Also, add media attributes to the links. * - * @return array Links with adjacent-duplicates merged together. + * @return array Prepared links with adjacent-duplicates merged together and media attributes added. */ - private function get_adjacent_deduplicated_links(): array { + private function prepare_links(): array { $links = $this->links; usort( @@ -100,7 +100,8 @@ static function ( array $a, array $b ): int { } ); - return array_reduce( + // Deduplicating adjacent links. + $prepared_links = array_reduce( $links, /** * Reducer. @@ -125,7 +126,7 @@ static function ( array $carry, array $link ): array { ) { $last_link['maximum_viewport_width'] = max( $last_link['maximum_viewport_width'], $link['maximum_viewport_width'] ); - // Update the last link with the new maximum viewport with. + // Update the last link with the new maximum viewport width. $carry[ count( $carry ) - 1 ] = $last_link; } else { $carry[] = $link; @@ -134,27 +135,20 @@ static function ( array $carry, array $link ): array { }, array() ); - } - /** - * Adds media features to the links. - * - * @phpstan-param Link $link - * @param array $link Link array. - * @phpstan-return Link - * @return array Link array with media features added. - */ - private function add_media_features( array $link ): array { - $media_features = array( 'screen' ); - if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { - $media_features[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); - } - if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { - $media_features[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); + // Add media attributes to the deduplicated links. + foreach ( $prepared_links as &$link ) { + $media_attributes = array( 'screen' ); + if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { + $media_attributes[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); + } + if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { + $media_attributes[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); + } + $link['attributes']['media'] = implode( ' and ', $media_attributes ); } - $link['attributes']['media'] = implode( ' and ', $media_features ); - return (array) $link; + return $prepared_links; } /** @@ -165,9 +159,7 @@ private function add_media_features( array $link ): array { public function get_html(): string { $link_tags = array(); - foreach ( $this->get_adjacent_deduplicated_links() as $link ) { - $link = $this->add_media_features( (array) $link ); - + foreach ( $this->prepare_links() as $link ) { $link_tag = ' $value ) { $link_tag .= sprintf( ' %s="%s"', $name, esc_attr( $value ) ); @@ -188,9 +180,7 @@ public function get_html(): string { public function get_response_header(): ?string { $link_headers = array(); - foreach ( $this->get_adjacent_deduplicated_links() as $link ) { - $link = $this->add_media_features( (array) $link ); - + foreach ( $this->prepare_links() as $link ) { $link_header = '<' . esc_url_raw( $link['attributes']['href'] ?? '' ) . '>; rel="preload"'; foreach ( $link['attributes'] as $name => $value ) { if ( 'href' !== $name ) { From c3acee819e3aac37b9c02e67fdee99654d806b03 Mon Sep 17 00:00:00 2001 From: AhmarZaidi Date: Fri, 5 Jul 2024 01:27:28 +0530 Subject: [PATCH 4/8] PR Changes - Update documentation - Fix preload link without href using about:blank fallback --- .../class-od-preload-link-collection.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index a7c92d02e0..5968042901 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -76,14 +76,14 @@ public function add_link( array $attributes, ?int $minimum_viewport_width, ?int } /** - * Prepare links by deduplicating adjacent links and adding media attributes. + * Prepares links by deduplicating adjacent links and adding media attributes. * * When two links are identical except for their minimum/maximum widths which are also consecutive, then merge them * together. Also, add media attributes to the links. * * @return array Prepared links with adjacent-duplicates merged together and media attributes added. */ - private function prepare_links(): array { + private function get_prepared_links(): array { $links = $this->links; usort( @@ -100,7 +100,7 @@ static function ( array $a, array $b ): int { } ); - // Deduplicating adjacent links. + // Deduplicate adjacent links. $prepared_links = array_reduce( $links, /** @@ -159,7 +159,7 @@ static function ( array $carry, array $link ): array { public function get_html(): string { $link_tags = array(); - foreach ( $this->prepare_links() as $link ) { + foreach ( $this->get_prepared_links() as $link ) { $link_tag = ' $value ) { $link_tag .= sprintf( ' %s="%s"', $name, esc_attr( $value ) ); @@ -180,12 +180,13 @@ public function get_html(): string { public function get_response_header(): ?string { $link_headers = array(); - foreach ( $this->prepare_links() as $link ) { - $link_header = '<' . esc_url_raw( $link['attributes']['href'] ?? '' ) . '>; rel="preload"'; + foreach ( $this->get_prepared_links() as $link ) { + // The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded. + $link['attributes']['href'] = isset( $link['attributes']['href'] ) ? esc_url_raw( $link['attributes']['href'] ) : 'about:blank'; + $link_header = '<' . $link['attributes']['href'] . '>; rel="preload"'; + unset( $link['attributes']['href'] ); foreach ( $link['attributes'] as $name => $value ) { - if ( 'href' !== $name ) { - $link_header .= sprintf( '; %s="%s"', $name, rawurlencode( $value ) ); - } + $link_header .= sprintf( '; %s="%s"', $name, rawurlencode( $value ) ); } $link_headers[] = $link_header; From 9a47aaea920a98ad4789a936d756e00545a0437c Mon Sep 17 00:00:00 2001 From: AhmarZaidi Date: Sun, 7 Jul 2024 22:25:29 +0530 Subject: [PATCH 5/8] Add test for http link response headers --- .../tests/test-optimization.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php index 613265dc52..9ee1bbb2a3 100644 --- a/plugins/optimization-detective/tests/test-optimization.php +++ b/plugins/optimization-detective/tests/test-optimization.php @@ -368,6 +368,44 @@ function ( OD_HTML_Tag_Walker $walker, OD_URL_Metrics_Group_Collection $url_metr $this->assertEquals( $expected, $buffer ); } + /** + * Test get_response_header(). + * + * @covers \OD_Preload_Link_Collection::get_response_header + */ + public function test_get_response_header(): void { + $collection = new OD_Preload_Link_Collection(); + + $collection->add_link( + array( + 'href' => 'https://example.com/foo.jpg', + 'as' => 'image', + 'fetchpriority' => 'high', + 'imagesrcset' => 'https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w', + 'imagesizes' => '(max-width: 600px) 480px, 800px', + 'crossorigin' => 'anonymous', + ), + null, + null + ); + + $collection->add_link( + array( + 'href' => 'https://example.com/bar.jpg', + 'as' => 'image', + 'fetchpriority' => 'high', + 'imagesrcset' => 'https://example.com/bar-480w.jpg 480w, https://example.com/bar-800w.jpg 800w', + 'imagesizes' => '(max-width: 600px) 480px, 800px', + 'crossorigin' => 'anonymous', + ), + 600, + 1200 + ); + + $expected_header = 'Link: ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https%3A%2F%2Fexample.com%2Ffoo-480w.jpg%20480w%2C%20https%3A%2F%2Fexample.com%2Ffoo-800w.jpg%20800w"; imagesizes="%28max-width%3A%20600px%29%20480px%2C%20800px"; crossorigin="anonymous"; media="screen", ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https%3A%2F%2Fexample.com%2Fbar-480w.jpg%20480w%2C%20https%3A%2F%2Fexample.com%2Fbar-800w.jpg%20800w"; imagesizes="%28max-width%3A%20600px%29%20480px%2C%20800px"; crossorigin="anonymous"; media="screen%20and%20%28min-width%3A%20600px%29%20and%20%28max-width%3A%201200px%29"'; + $this->assertSame( $expected_header, $collection->get_response_header() ); + } + /** * Gets a validated URL metric. * From f2fbe38324fe6d9af1db42a884b437db16c7227c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 8 Jul 2024 13:12:07 -0700 Subject: [PATCH 6/8] Remove unnecessary global namespace backslash --- plugins/optimization-detective/tests/test-optimization.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php index 9ee1bbb2a3..f3c1c820d4 100644 --- a/plugins/optimization-detective/tests/test-optimization.php +++ b/plugins/optimization-detective/tests/test-optimization.php @@ -371,7 +371,7 @@ function ( OD_HTML_Tag_Walker $walker, OD_URL_Metrics_Group_Collection $url_metr /** * Test get_response_header(). * - * @covers \OD_Preload_Link_Collection::get_response_header + * @covers OD_Preload_Link_Collection::get_response_header */ public function test_get_response_header(): void { $collection = new OD_Preload_Link_Collection(); From d1cd8d47375fff452756e7337c1adaa1229da0fc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 8 Jul 2024 13:42:18 -0700 Subject: [PATCH 7/8] Escape quoted strings according to RFC 9110 --- .../class-od-preload-link-collection.php | 14 +++++++++++++- .../tests/test-optimization.php | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index 5968042901..fb7e9bff34 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -186,7 +186,19 @@ public function get_response_header(): ?string { $link_header = '<' . $link['attributes']['href'] . '>; rel="preload"'; unset( $link['attributes']['href'] ); foreach ( $link['attributes'] as $name => $value ) { - $link_header .= sprintf( '; %s="%s"', $name, rawurlencode( $value ) ); + /* + * Escape the value being put into an HTTP quoted string. The grammar is: + * + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + * + * See . So to escape a value we need to add + * a backslash in front of anything character which is not qdtext. + */ + $escaped_value = preg_replace( '/(?=[^\t \x21\x23-\x5B\x5D-\x7E\x80-\xFF])/', '\\\\', $value ); + $link_header .= sprintf( '; %s="%s"', $name, $escaped_value ); } $link_headers[] = $link_header; diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php index f3c1c820d4..ea3480cdbc 100644 --- a/plugins/optimization-detective/tests/test-optimization.php +++ b/plugins/optimization-detective/tests/test-optimization.php @@ -394,7 +394,7 @@ public function test_get_response_header(): void { 'href' => 'https://example.com/bar.jpg', 'as' => 'image', 'fetchpriority' => 'high', - 'imagesrcset' => 'https://example.com/bar-480w.jpg 480w, https://example.com/bar-800w.jpg 800w', + 'imagesrcset' => 'https://example.com/"bar"-480w.jpg 480w, https://example.com/"bar"-800w.jpg 800w', 'imagesizes' => '(max-width: 600px) 480px, 800px', 'crossorigin' => 'anonymous', ), @@ -402,7 +402,7 @@ public function test_get_response_header(): void { 1200 ); - $expected_header = 'Link: ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https%3A%2F%2Fexample.com%2Ffoo-480w.jpg%20480w%2C%20https%3A%2F%2Fexample.com%2Ffoo-800w.jpg%20800w"; imagesizes="%28max-width%3A%20600px%29%20480px%2C%20800px"; crossorigin="anonymous"; media="screen", ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https%3A%2F%2Fexample.com%2Fbar-480w.jpg%20480w%2C%20https%3A%2F%2Fexample.com%2Fbar-800w.jpg%20800w"; imagesizes="%28max-width%3A%20600px%29%20480px%2C%20800px"; crossorigin="anonymous"; media="screen%20and%20%28min-width%3A%20600px%29%20and%20%28max-width%3A%201200px%29"'; + $expected_header = 'Link: ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w"; imagesizes="(max-width: 600px) 480px, 800px"; crossorigin="anonymous"; media="screen", ; rel="preload"; as="image"; fetchpriority="high"; imagesrcset="https://example.com/\"bar\"-480w.jpg 480w, https://example.com/\"bar\"-800w.jpg 800w"; imagesizes="(max-width: 600px) 480px, 800px"; crossorigin="anonymous"; media="screen and (min-width: 600px) and (max-width: 1200px)"'; $this->assertSame( $expected_header, $collection->get_response_header() ); } From f4ab19acbadcaac5bd9cb00a65a7c9615a0704f2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 8 Jul 2024 13:45:53 -0700 Subject: [PATCH 8/8] Use array_map() instead of reference --- .../class-od-preload-link-collection.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/optimization-detective/class-od-preload-link-collection.php b/plugins/optimization-detective/class-od-preload-link-collection.php index fb7e9bff34..91f47850d8 100644 --- a/plugins/optimization-detective/class-od-preload-link-collection.php +++ b/plugins/optimization-detective/class-od-preload-link-collection.php @@ -137,18 +137,20 @@ static function ( array $carry, array $link ): array { ); // Add media attributes to the deduplicated links. - foreach ( $prepared_links as &$link ) { - $media_attributes = array( 'screen' ); - if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { - $media_attributes[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); - } - if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { - $media_attributes[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); - } - $link['attributes']['media'] = implode( ' and ', $media_attributes ); - } - - return $prepared_links; + return array_map( + static function ( array $link ): array { + $media_attributes = array( 'screen' ); + if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { + $media_attributes[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); + } + if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { + $media_attributes[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); + } + $link['attributes']['media'] = implode( ' and ', $media_attributes ); + return $link; + }, + $prepared_links + ); } /**