14
14
/**
15
15
* Tag visitor that optimizes IMG tags.
16
16
*
17
+ * @phpstan-import-type LinkAttributes from OD_Link_Collection
18
+ *
17
19
* @since 0.1.0
18
20
* @access private
19
21
*/
@@ -22,19 +24,37 @@ final class Image_Prioritizer_Img_Tag_Visitor extends Image_Prioritizer_Tag_Visi
22
24
/**
23
25
* Visits a tag.
24
26
*
25
- * @param OD_Tag_Visitor_Context $context Tag visitor context.
27
+ * @since 0.1.0
28
+ * @since n.e.x.t Separate the processing of IMG and PICTURE elements.
26
29
*
30
+ * @param OD_Tag_Visitor_Context $context Tag visitor context.
27
31
* @return bool Whether the tag should be tracked in URL Metrics.
28
32
*/
29
33
public function __invoke ( OD_Tag_Visitor_Context $ context ): bool {
30
34
$ processor = $ context ->processor ;
31
- if ( 'IMG ' !== $ processor ->get_tag () ) {
32
- return false ;
35
+ $ tag = $ processor ->get_tag ();
36
+
37
+ if ( 'PICTURE ' === $ tag ) {
38
+ return $ this ->process_picture ( $ processor , $ context );
39
+ } elseif ( 'IMG ' === $ tag ) {
40
+ return $ this ->process_img ( $ processor , $ context );
33
41
}
34
42
35
- // Skip empty src attributes and data: URLs.
36
- $ src = trim ( (string ) $ processor ->get_attribute ( 'src ' ) );
37
- if ( '' === $ src || $ this ->is_data_url ( $ src ) ) {
43
+ return false ;
44
+ }
45
+
46
+ /**
47
+ * Process an IMG element.
48
+ *
49
+ * @since n.e.x.t
50
+ *
51
+ * @param OD_HTML_Tag_Processor $processor HTML tag processor.
52
+ * @param OD_Tag_Visitor_Context $context Tag visitor context.
53
+ * @return bool Whether the tag should be tracked in URL Metrics.
54
+ */
55
+ private function process_img ( OD_HTML_Tag_Processor $ processor , OD_Tag_Visitor_Context $ context ): bool {
56
+ $ src = $ this ->get_valid_src ( $ processor );
57
+ if ( null === $ src ) {
38
58
return false ;
39
59
}
40
60
@@ -142,41 +162,207 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
142
162
}
143
163
}
144
164
145
- // If this element is the LCP (for a breakpoint group), add a preload link for it.
146
- foreach ( $ context ->url_metric_group_collection ->get_groups_by_lcp_element ( $ xpath ) as $ group ) {
147
- $ link_attributes = array_merge (
165
+ $ parent_tag = $ this ->get_parent_tag_name ( $ context );
166
+ if ( 'PICTURE ' !== $ parent_tag ) {
167
+ $ this ->add_image_preload_link_for_lcp_element_groups (
168
+ $ context ,
169
+ $ xpath ,
148
170
array (
149
- 'rel ' => 'preload ' ,
150
- 'fetchpriority ' => 'high ' ,
151
- 'as ' => 'image ' ,
152
- ),
153
- array_filter (
154
- array (
155
- 'href ' => (string ) $ processor ->get_attribute ( 'src ' ),
156
- 'imagesrcset ' => (string ) $ processor ->get_attribute ( 'srcset ' ),
157
- 'imagesizes ' => (string ) $ processor ->get_attribute ( 'sizes ' ),
158
- ),
159
- static function ( string $ value ): bool {
160
- return '' !== $ value ;
161
- }
171
+ 'href ' => $ processor ->get_attribute ( 'src ' ),
172
+ 'imagesrcset ' => $ processor ->get_attribute ( 'srcset ' ),
173
+ 'imagesizes ' => $ processor ->get_attribute ( 'sizes ' ),
174
+ 'crossorigin ' => $ this ->get_attribute_value ( $ processor , 'crossorigin ' ),
175
+ 'referrerpolicy ' => $ this ->get_attribute_value ( $ processor , 'referrerpolicy ' ),
162
176
)
163
177
);
178
+ }
179
+
180
+ return true ;
181
+ }
182
+
183
+ /**
184
+ * Process a PICTURE element.
185
+ *
186
+ * @since n.e.x.t
187
+ *
188
+ * @param OD_HTML_Tag_Processor $processor HTML tag processor.
189
+ * @param OD_Tag_Visitor_Context $context Tag visitor context.
190
+ * @return bool Whether the tag should be tracked in URL Metrics.
191
+ */
192
+ private function process_picture ( OD_HTML_Tag_Processor $ processor , OD_Tag_Visitor_Context $ context ): bool {
193
+ /**
194
+ * First SOURCE tag's attributes.
195
+ *
196
+ * @var array{ srcset: non-empty-string, sizes: string|null, type: non-empty-string }|null $first_source
197
+ */
198
+ $ first_source = null ;
199
+ $ img_xpath = null ;
200
+
201
+ $ referrerpolicy = null ;
202
+ $ crossorigin = null ;
203
+
204
+ // Loop through child tags until we reach the closing PICTURE tag.
205
+ while ( $ processor ->next_tag () ) {
206
+ $ tag = $ processor ->get_tag ();
207
+
208
+ // If we reached the closing PICTURE tag, break.
209
+ if ( 'PICTURE ' === $ tag && $ processor ->is_tag_closer () ) {
210
+ break ;
211
+ }
212
+
213
+ // Process the SOURCE elements.
214
+ if ( 'SOURCE ' === $ tag && ! $ processor ->is_tag_closer () ) {
215
+ // Abort processing if the PICTURE involves art direction since then adding a preload link is infeasible.
216
+ if ( null !== $ processor ->get_attribute ( 'media ' ) ) {
217
+ return false ;
218
+ }
219
+
220
+ // Abort processing if a SOURCE lacks the required srcset attribute.
221
+ $ srcset = $ this ->get_valid_src ( $ processor , 'srcset ' );
222
+ if ( null === $ srcset ) {
223
+ return false ;
224
+ }
225
+
226
+ // Abort processing if there is no valid image type.
227
+ $ type = $ this ->get_attribute_value ( $ processor , 'type ' );
228
+ if ( ! is_string ( $ type ) || ! str_starts_with ( $ type , 'image/ ' ) ) {
229
+ return false ;
230
+ }
231
+
232
+ // Collect the first valid SOURCE as the preload link.
233
+ if ( null === $ first_source ) {
234
+ $ sizes = $ processor ->get_attribute ( 'sizes ' );
235
+ $ first_source = array (
236
+ 'srcset ' => $ srcset ,
237
+ 'sizes ' => is_string ( $ sizes ) ? $ sizes : null ,
238
+ 'type ' => $ type ,
239
+ );
240
+ }
241
+ }
242
+
243
+ // Process the IMG element within the PICTURE.
244
+ if ( 'IMG ' === $ tag && ! $ processor ->is_tag_closer () ) {
245
+ $ src = $ this ->get_valid_src ( $ processor );
246
+ if ( null === $ src ) {
247
+ return false ;
248
+ }
164
249
165
- $ crossorigin = $ this ->get_attribute_value ( $ processor , 'crossorigin ' );
166
- if ( null !== $ crossorigin ) {
167
- $ link_attributes ['crossorigin ' ] = 'use-credentials ' === $ crossorigin ? 'use-credentials ' : 'anonymous ' ;
250
+ // These attributes are only defined on the IMG itself.
251
+ $ referrerpolicy = $ this ->get_attribute_value ( $ processor , 'referrerpolicy ' );
252
+ $ crossorigin = $ this ->get_attribute_value ( $ processor , 'crossorigin ' );
253
+
254
+ // Capture the XPath for the IMG since the browser captures it as the LCP element, so we need this to
255
+ // look up whether it is the LCP element in the URL Metric groups.
256
+ $ img_xpath = $ processor ->get_xpath ();
168
257
}
258
+ }
169
259
170
- $ link_attributes ['media ' ] = 'screen ' ;
260
+ // Abort if we never encountered a SOURCE or IMG tag.
261
+ if ( null === $ img_xpath || null === $ first_source ) {
262
+ return false ;
263
+ }
264
+
265
+ $ this ->add_image_preload_link_for_lcp_element_groups (
266
+ $ context ,
267
+ $ img_xpath ,
268
+ array (
269
+ 'imagesrcset ' => $ first_source ['srcset ' ],
270
+ 'imagesizes ' => $ first_source ['sizes ' ],
271
+ 'type ' => $ first_source ['type ' ],
272
+ 'crossorigin ' => $ crossorigin ,
273
+ 'referrerpolicy ' => $ referrerpolicy ,
274
+ )
275
+ );
276
+
277
+ return false ;
278
+ }
171
279
280
+ /**
281
+ * Gets valid src attribute value for preloading.
282
+ *
283
+ * Returns null if the src attribute is not a string (i.e. src was used as a boolean attribute was used), if it
284
+ * it has an empty string value after trimming, or if it is a data: URL.
285
+ *
286
+ * @since n.e.x.t
287
+ *
288
+ * @param OD_HTML_Tag_Processor $processor Processor.
289
+ * @param 'src'|'srcset' $attribute_name Attribute name.
290
+ * @return non-empty-string|null URL which is not a data: URL.
291
+ */
292
+ private function get_valid_src ( OD_HTML_Tag_Processor $ processor , string $ attribute_name = 'src ' ): ?string {
293
+ $ src = $ processor ->get_attribute ( $ attribute_name );
294
+ if ( ! is_string ( $ src ) ) {
295
+ return null ;
296
+ }
297
+ $ src = trim ( $ src );
298
+ if ( '' === $ src || $ this ->is_data_url ( $ src ) ) {
299
+ return null ;
300
+ }
301
+ return $ src ;
302
+ }
303
+
304
+ /**
305
+ * Adds a LINK with the supplied attributes for each viewport group when the provided XPath is the LCP element.
306
+ *
307
+ * @since n.e.x.t
308
+ *
309
+ * @param OD_Tag_Visitor_Context $context Tag visitor context.
310
+ * @param string $xpath XPath of the element.
311
+ * @param array<string, string|true|null> $attributes Attributes to add to the link.
312
+ */
313
+ private function add_image_preload_link_for_lcp_element_groups ( OD_Tag_Visitor_Context $ context , string $ xpath , array $ attributes ): void {
314
+ $ attributes = array_filter (
315
+ $ attributes ,
316
+ static function ( $ attribute_value ) {
317
+ return is_string ( $ attribute_value ) && '' !== $ attribute_value ;
318
+ }
319
+ );
320
+
321
+ /**
322
+ * Link attributes.
323
+ *
324
+ * This type is needed because PHPStan isn't apparently aware of the new keys added after the array_merge().
325
+ * Note that there is no type checking being done on the attributes above other than ensuring they are
326
+ * non-empty-strings.
327
+ *
328
+ * @var LinkAttributes $attributes
329
+ */
330
+ $ attributes = array_merge (
331
+ array (
332
+ 'rel ' => 'preload ' ,
333
+ 'fetchpriority ' => 'high ' ,
334
+ 'as ' => 'image ' ,
335
+ ),
336
+ $ attributes ,
337
+ array (
338
+ 'media ' => 'screen ' ,
339
+ )
340
+ );
341
+
342
+ foreach ( $ context ->url_metric_group_collection ->get_groups_by_lcp_element ( $ xpath ) as $ group ) {
172
343
$ context ->link_collection ->add_link (
173
- $ link_attributes ,
344
+ $ attributes ,
174
345
$ group ->get_minimum_viewport_width (),
175
346
$ group ->get_maximum_viewport_width ()
176
347
);
177
348
}
349
+ }
178
350
179
- return true ;
351
+ /**
352
+ * Gets the parent tag name.
353
+ *
354
+ * @since n.e.x.t
355
+ *
356
+ * @param OD_Tag_Visitor_Context $context Tag visitor context.
357
+ * @return string|null The parent tag name or null if not found.
358
+ */
359
+ private function get_parent_tag_name ( OD_Tag_Visitor_Context $ context ): ?string {
360
+ $ breadcrumbs = $ context ->processor ->get_breadcrumbs ();
361
+ $ length = count ( $ breadcrumbs );
362
+ if ( $ length < 2 ) {
363
+ return null ;
364
+ }
365
+ return $ breadcrumbs [ $ length - 2 ];
180
366
}
181
367
182
368
/**
0 commit comments