@automattic/jetpack-image-guide 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2023-07-07
9
+ ### Added
10
+ - Boost: exclude small images from Image Size Analysis [#31504]
11
+
12
+ ### Changed
13
+ - Return an error when an image is missing, instead of -1 by -1 dimensions. [#31632]
14
+
15
+ ### Fixed
16
+ - Check response.url, not response.ok to verify a response worked [#31538]
17
+
18
+ ## [0.4.0] - 2023-06-23
19
+ ### Added
20
+ - Jetpack Boost: add a proxy to Image Guide so it can load remote images. [#31145]
21
+
22
+ ### Fixed
23
+ - Improved image url validation for background image source. [#31410]
24
+
8
25
  ## 0.3.0 - 2023-05-11
9
26
  ### Changed
10
27
  - Set `exports` in package.json. This will break directly requiring files from within the package in environments that respect `exports`. [#30313]
@@ -41,3 +58,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
58
 
42
59
  ### Removed
43
60
  - Minor package.json change - removing private entry.
61
+
62
+ [0.5.0]: https://github.com/Automattic/jetpack-image-guide/compare/v0.4.0...v0.5.0
63
+ [0.4.0]: https://github.com/Automattic/jetpack-image-guide/compare/v0.3.0...v0.4.0
@@ -6,6 +6,7 @@ export type Dimensions = {
6
6
  export type Weight = {
7
7
  weight: number;
8
8
  };
9
+ export type FetchFn = (input: URL | RequestInfo, init?: RequestInit) => Promise<Response>;
9
10
  /**
10
11
  * A class that represents a DOM Element that
11
12
  * has an image that should be measured and
@@ -14,13 +15,15 @@ export type Weight = {
14
15
  export declare class MeasurableImage {
15
16
  readonly node: HTMLElement | HTMLImageElement;
16
17
  private getURLCallback;
18
+ fetch: FetchFn;
17
19
  /**
18
20
  * Constructor.
19
21
  *
20
22
  * @param {HTMLElement | HTMLImageElement} node - The DOM Element that contains the image.
21
23
  * @param {SourceCallbackFn} getURL - A function that takes in the node and returns the URL of the image.
24
+ * @param {FetchFn} fetchFn - A function that fetches a URL and returns a Promise.
22
25
  */
23
- constructor(node: HTMLElement | HTMLImageElement, getURL: SourceCallbackFn);
26
+ constructor(node: HTMLElement | HTMLImageElement, getURL: SourceCallbackFn, fetchFn?: FetchFn | null);
24
27
  getURL(): string;
25
28
  getSizeOnPage(): {
26
29
  width: number;
@@ -58,4 +61,11 @@ export declare class MeasurableImage {
58
61
  * @param {string} url - image url
59
62
  */
60
63
  private fetchFileDimensions;
64
+ /**
65
+ * Checks if the image is too small and should be ignored. Will return false on images
66
+ * that don't load at all - we can't establish they're tiny!
67
+ *
68
+ * @returns {boolean} - if the image is smaller than 65 pixels width and height return true
69
+ */
70
+ isImageTiny(): Promise<boolean>;
61
71
  }
@@ -1,4 +1,4 @@
1
- import { MeasurableImage } from './MeasurableImage';
1
+ import { FetchFn, MeasurableImage } from './MeasurableImage';
2
2
  /**
3
3
  * Get elements that either are image tags or have a background image.
4
4
  *
@@ -25,6 +25,7 @@ export declare function backgroundImageSource(node: HTMLElement): string;
25
25
  * and remove any nodes that can't be measured.
26
26
  *
27
27
  * @param {Element[]} domNodes - A list of nodes to measure
28
+ * @param {(input: string, init?: Array) => Promise<Response>} fetchFn - A function that fetches a URL and returns a Promise.
28
29
  * @returns {MeasurableImage[]} - A list of MeasurableImage objects.
29
30
  */
30
- export declare function getMeasurableImages(domNodes: Element[]): MeasurableImage[];
31
+ export declare function getMeasurableImages(domNodes: Element[], fetchFn?: FetchFn | null): Promise<MeasurableImage[]>;
@@ -1 +1 @@
1
- <?php return array('dependencies' => array(), 'version' => '5970656af8067fd278a2');
1
+ <?php return array('dependencies' => array(), 'version' => '001cd98ea4ed0b8489db');
package/build/index.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ImageGuideLibrary=e():t.ImageGuideLibrary=e()}(self,(()=>(()=>{"use strict";var t={38:(t,e,n)=>{n.d(e,{z:()=>i});var r=function(t,e,n,r){return new(n||(n=Promise))((function(o,i){function u(t){try{c(r.next(t))}catch(t){i(t)}}function a(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(u,a)}c((r=r.apply(t,e||[])).next())}))},o=function(t,e){var n,r,o,i,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(a){return function(c){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,a[0]&&(u=0)),u;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return u.label++,{value:a[1],done:!1};case 5:u.label++,r=a[1],a=[0];continue;case 7:a=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==a[0]&&2!==a[0])){u=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]<o[3])){u.label=a[1];break}if(6===a[0]&&u.label<o[1]){u.label=o[1],o=a;break}if(o&&u.label<o[2]){u.label=o[2],u.ops.push(a);break}o[2]&&u.ops.pop(),u.trys.pop();continue}a=e.call(t,u)}catch(t){a=[6,t],r=0}finally{n=o=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}},i=function(){function t(t,e){this.node=t,this.getURLCallback=e}return t.prototype.getURL=function(){return this.getURLCallback(this.node)},t.prototype.getSizeOnPage=function(){var t=this.node.getBoundingClientRect(),e=t.width,n=t.height;return{width:Math.round(e),height:Math.round(n)}},t.prototype.getFileSize=function(t){return r(this,void 0,void 0,(function(){var e,n,r,i,u;return o(this,(function(o){switch(o.label){case 0:return[4,Promise.all([this.fetchFileWeight(t),this.fetchFileDimensions(t)])];case 1:return e=o.sent(),n=e[0],r=e[1],i=r.width,u=r.height,[2,{width:i,height:u,weight:n}]}}))}))},t.prototype.getPotentialSavings=function(t,e){var n=this.getOversizedRatio(t,e);return n<=1?null:Math.round(t.weight-t.weight/n)},t.prototype.getExpectedSize=function(t){var e=window.devicePixelRatio||1;return{width:Math.round(t.width*e),height:Math.round(t.height*e)}},t.prototype.getOversizedRatio=function(t,e){var n=this.getExpectedSize(e),r=n.width,o=n.height;return t.width*t.height/(r*o)},t.prototype.fetchFileWeight=function(t){return r(this,void 0,void 0,(function(){var e,n;return o(this,(function(r){switch(r.label){case 0:return[4,fetch(t,{method:"HEAD",mode:"no-cors"})];case 1:return(e=r.sent()).url?(n=e.headers.get("content-length"))?[2,parseInt(n,10)/1024]:[2,-1]:(console.log("Can't get image size for ".concat(t," likely due to a CORS error.")),[2,-1])}}))}))},t.prototype.fetchFileDimensions=function(t){return r(this,void 0,void 0,(function(){var e;return o(this,(function(n){return(e=new Image).src=t,[2,new Promise((function(t){e.onload=function(){t({width:Math.round(e.width),height:Math.round(e.height)})},e.onerror=function(){t({width:-1,height:-1})}}))]}))}))},t}()},705:(t,e,n)=>{n.d(e,{RK:()=>u});var r=n(38);function o(t){return a(t.currentSrc)?t.currentSrc:a(t.src)?t.src:null}function i(t){var e=getComputedStyle(t).backgroundImage.match(/url\(.?(.*?).?\)/i);return e&&e[1]&&a(e[1])?e[1]:null}function u(t){return function(t){return t.filter((function(t){return t instanceof HTMLImageElement||t instanceof HTMLElement&&"none"!==getComputedStyle(t).backgroundImage}))}(t).map((function(t){return t instanceof HTMLImageElement?new r.z(t,o):t instanceof HTMLElement&&i(t)?new r.z(t,i):null})).filter((function(t){return null!==t}))}function a(t){if(t.startsWith("/"))return!t.split("?")[0].endsWith(".svg");try{var e=new URL(t);return"http:"===e.protocol||"https:"===e.protocol}catch(t){return!1}}}},e={};function n(r){var o=e[r];if(void 0!==o)return o.exports;var i=e[r]={exports:{}};return t[r](i,i.exports,n),i.exports}n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};return(()=>{n.r(r),n.d(r,{MeasurableImage:()=>t.z,getMeasurableImages:()=>e.RK});var t=n(38),e=n(705)})(),r})()));
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ImageGuideLibrary=e():t.ImageGuideLibrary=e()}(self,(()=>(()=>{"use strict";var t={38:(t,e,n)=>{n.d(e,{z:()=>i});var r=function(t,e,n,r){return new(n||(n=Promise))((function(o,i){function u(t){try{c(r.next(t))}catch(t){i(t)}}function a(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(u,a)}c((r=r.apply(t,e||[])).next())}))},o=function(t,e){var n,r,o,i,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(a){return function(c){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,a[0]&&(u=0)),u;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return u.label++,{value:a[1],done:!1};case 5:u.label++,r=a[1],a=[0];continue;case 7:a=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==a[0]&&2!==a[0])){u=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]<o[3])){u.label=a[1];break}if(6===a[0]&&u.label<o[1]){u.label=o[1],o=a;break}if(o&&u.label<o[2]){u.label=o[2],u.ops.push(a);break}o[2]&&u.ops.pop(),u.trys.pop();continue}a=e.call(t,u)}catch(t){a=[6,t],r=0}finally{n=o=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}},i=function(){function t(t,e,n){void 0===n&&(n=null),this.node=t,this.getURLCallback=e,this.fetch=null!=n?n:window.fetch.bind(window)}return t.prototype.getURL=function(){return this.getURLCallback(this.node)},t.prototype.getSizeOnPage=function(){var t=this.node.getBoundingClientRect(),e=t.width,n=t.height;return{width:Math.round(e),height:Math.round(n)}},t.prototype.getFileSize=function(t){return r(this,void 0,void 0,(function(){var e,n,r,i,u;return o(this,(function(o){switch(o.label){case 0:return[4,Promise.all([this.fetchFileWeight(t),this.fetchFileDimensions(t)])];case 1:return e=o.sent(),n=e[0],r=e[1],i=r.width,u=r.height,[2,{width:i,height:u,weight:n}]}}))}))},t.prototype.getPotentialSavings=function(t,e){var n=this.getOversizedRatio(t,e);return n<=1?null:Math.round(t.weight-t.weight/n)},t.prototype.getExpectedSize=function(t){var e=window.devicePixelRatio||1;return{width:Math.round(t.width*e),height:Math.round(t.height*e)}},t.prototype.getOversizedRatio=function(t,e){var n=this.getExpectedSize(e),r=n.width,o=n.height;return t.width*t.height/(r*o)},t.prototype.fetchFileWeight=function(t){return r(this,void 0,void 0,(function(){var e;return o(this,(function(n){switch(n.label){case 0:return[4,this.fetch(t)];case 1:return(e=n.sent()).ok?[2,parseInt(e.headers.get("content-length"),10)/1024]:(console.log("Can't get image size for ".concat(t," likely due to a CORS error.")),[2,-1])}}))}))},t.prototype.fetchFileDimensions=function(t){return r(this,void 0,void 0,(function(){var e;return o(this,(function(n){return(e=new Image).src=t,[2,new Promise((function(t,n){e.onload=function(){t({width:Math.round(e.width),height:Math.round(e.height)})},e.onerror=function(){n("Unable to load image.")}}))]}))}))},t.prototype.isImageTiny=function(){return r(this,void 0,void 0,(function(){var t,e;return o(this,(function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),t=65,[4,this.fetchFileDimensions(this.getURL())];case 1:return[2,(e=n.sent()).width<t||e.height<t];case 2:return n.sent(),[2,!1];case 3:return[2]}}))}))},t}()},705:(t,e,n)=>{n.d(e,{RK:()=>c});var r=n(38),o=function(t,e,n,r){return new(n||(n=Promise))((function(o,i){function u(t){try{c(r.next(t))}catch(t){i(t)}}function a(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(u,a)}c((r=r.apply(t,e||[])).next())}))},i=function(t,e){var n,r,o,i,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(a){return function(c){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,a[0]&&(u=0)),u;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return u.label++,{value:a[1],done:!1};case 5:u.label++,r=a[1],a=[0];continue;case 7:a=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==a[0]&&2!==a[0])){u=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]<o[3])){u.label=a[1];break}if(6===a[0]&&u.label<o[1]){u.label=o[1],o=a;break}if(o&&u.label<o[2]){u.label=o[2],u.ops.push(a);break}o[2]&&u.ops.pop(),u.trys.pop();continue}a=e.call(t,u)}catch(t){a=[6,t],r=0}finally{n=o=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}};function u(t){return l(t.currentSrc)?t.currentSrc:l(t.src)?t.src:null}function a(t){var e=getComputedStyle(t).backgroundImage.match(/url\(\s*(['"])(.*?)\1\s*\)/i),n=window.location.href.split("#")[0];return e&&e[2]&&e[2]!==n&&l(e[2])?e[2]:null}function c(t,e){return void 0===e&&(e=null),o(this,void 0,void 0,(function(){var n;return i(this,(function(o){return n=function(t){return t.filter((function(t){return t instanceof HTMLImageElement||t instanceof HTMLElement&&"none"!==getComputedStyle(t).backgroundImage}))}(t),[2,n.map((function(t){return t instanceof HTMLImageElement?new r.z(t,u,e):t instanceof HTMLElement&&a(t)?new r.z(t,a):null}))]}))}))}function l(t){if(t.startsWith("/"))return!t.split("?")[0].endsWith(".svg");try{var e=new URL(t);return"http:"===e.protocol||"https:"===e.protocol}catch(t){return!1}}}},e={};function n(r){var o=e[r];if(void 0!==o)return o.exports;var i=e[r]={exports:{}};return t[r](i,i.exports,n),i.exports}n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};return(()=>{n.r(r),n.d(r,{MeasurableImage:()=>t.z,getMeasurableImages:()=>e.RK});var t=n(38),e=n(705)})(),r})()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-image-guide",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Go through the dom to analyze image size on screen vs actual file size.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/image-guide/#readme",
6
6
  "type": "module",
@@ -2,6 +2,8 @@ export type SourceCallbackFn = ( node: HTMLElement ) => string | null;
2
2
  export type Dimensions = { width: number; height: number };
3
3
  export type Weight = { weight: number };
4
4
 
5
+ export type FetchFn = ( input: URL | RequestInfo, init?: RequestInit ) => Promise< Response >;
6
+
5
7
  /**
6
8
  * A class that represents a DOM Element that
7
9
  * has an image that should be measured and
@@ -11,15 +13,23 @@ export class MeasurableImage {
11
13
  readonly node: HTMLElement | HTMLImageElement;
12
14
  private getURLCallback: SourceCallbackFn;
13
15
 
16
+ public fetch: FetchFn;
17
+
14
18
  /**
15
19
  * Constructor.
16
20
  *
17
21
  * @param {HTMLElement | HTMLImageElement} node - The DOM Element that contains the image.
18
22
  * @param {SourceCallbackFn} getURL - A function that takes in the node and returns the URL of the image.
23
+ * @param {FetchFn} fetchFn - A function that fetches a URL and returns a Promise.
19
24
  */
20
- constructor( node: HTMLElement | HTMLImageElement, getURL: SourceCallbackFn ) {
25
+ constructor(
26
+ node: HTMLElement | HTMLImageElement,
27
+ getURL: SourceCallbackFn,
28
+ fetchFn: FetchFn | null = null
29
+ ) {
21
30
  this.node = node;
22
31
  this.getURLCallback = getURL;
32
+ this.fetch = fetchFn ?? window.fetch.bind( window );
23
33
  }
24
34
 
25
35
  public getURL() {
@@ -83,19 +93,14 @@ export class MeasurableImage {
83
93
  * @param {string} url - string The URL of the image.
84
94
  */
85
95
  private async fetchFileWeight( url: string ) {
86
- const response = await fetch( url, { method: 'HEAD', mode: 'no-cors' } );
87
- if ( ! response.url ) {
96
+ const response = await this.fetch( url );
97
+ if ( ! response.ok ) {
88
98
  // eslint-disable-next-line no-console
89
99
  console.log( `Can't get image size for ${ url } likely due to a CORS error.` );
90
100
  return -1;
91
101
  }
92
102
 
93
- const size = response.headers.get( 'content-length' );
94
- if ( size ) {
95
- return parseInt( size, 10 ) / 1024;
96
- }
97
-
98
- return -1;
103
+ return parseInt( response.headers.get( 'content-length' ), 10 ) / 1024;
99
104
  }
100
105
 
101
106
  /**
@@ -107,13 +112,29 @@ export class MeasurableImage {
107
112
  private async fetchFileDimensions( url: string ) {
108
113
  const img = new Image();
109
114
  img.src = url;
110
- return new Promise< { width: number; height: number } >( resolve => {
115
+ return new Promise< { width: number; height: number } >( ( resolve, reject ) => {
111
116
  img.onload = () => {
112
117
  resolve( { width: Math.round( img.width ), height: Math.round( img.height ) } );
113
118
  };
114
119
  img.onerror = () => {
115
- resolve( { width: -1, height: -1 } );
120
+ reject( 'Unable to load image.' );
116
121
  };
117
122
  } );
118
123
  }
124
+
125
+ /**
126
+ * Checks if the image is too small and should be ignored. Will return false on images
127
+ * that don't load at all - we can't establish they're tiny!
128
+ *
129
+ * @returns {boolean} - if the image is smaller than 65 pixels width and height return true
130
+ */
131
+ public async isImageTiny(): Promise< boolean > {
132
+ try {
133
+ const minSize = 65;
134
+ const dimensions = await this.fetchFileDimensions( this.getURL() );
135
+ return dimensions.width < minSize || dimensions.height < minSize;
136
+ } catch ( err ) {
137
+ return false;
138
+ }
139
+ }
119
140
  }
@@ -1,4 +1,4 @@
1
- import { MeasurableImage } from './MeasurableImage';
1
+ import { FetchFn, MeasurableImage } from './MeasurableImage';
2
2
 
3
3
  /**
4
4
  * Get elements that either are image tags or have a background image.
@@ -44,9 +44,13 @@ export function imageTagSource( node: HTMLImageElement ) {
44
44
  */
45
45
  export function backgroundImageSource( node: HTMLElement ) {
46
46
  const src = getComputedStyle( node ).backgroundImage;
47
- const url = src.match( /url\(.?(.*?).?\)/i );
48
- if ( url && url[ 1 ] && imageLikeURL( url[ 1 ] ) ) {
49
- return url[ 1 ];
47
+ const url = src.match( /url\(\s*(['"])(.*?)\1\s*\)/i );
48
+
49
+ // If background image is `url('')`, the computed value becomes the current page URL. So we need to check for that.
50
+ const currentUrlWithoutHash = window.location.href.split( '#' )[ 0 ];
51
+
52
+ if ( url && url[ 2 ] && url[ 2 ] !== currentUrlWithoutHash && imageLikeURL( url[ 2 ] ) ) {
53
+ return url[ 2 ];
50
54
  }
51
55
  return null;
52
56
  }
@@ -56,29 +60,33 @@ export function backgroundImageSource( node: HTMLElement ) {
56
60
  * and remove any nodes that can't be measured.
57
61
  *
58
62
  * @param {Element[]} domNodes - A list of nodes to measure
63
+ * @param {(input: string, init?: Array) => Promise<Response>} fetchFn - A function that fetches a URL and returns a Promise.
59
64
  * @returns {MeasurableImage[]} - A list of MeasurableImage objects.
60
65
  */
61
- export function getMeasurableImages( domNodes: Element[] ): MeasurableImage[] {
66
+ export async function getMeasurableImages(
67
+ domNodes: Element[],
68
+ fetchFn: FetchFn | null = null
69
+ ): Promise< MeasurableImage[] > {
62
70
  const nodes = findMeasurableElements( domNodes );
63
- return nodes
64
- .map( node => {
65
- if ( node instanceof HTMLImageElement ) {
66
- return new MeasurableImage( node, imageTagSource );
67
- } else if ( node instanceof HTMLElement ) {
68
- if ( ! backgroundImageSource( node ) ) {
69
- /**
70
- * Background elements that have no valid URL
71
- * shouldn't be measured.
72
- */
73
- return null;
74
- }
75
-
76
- return new MeasurableImage( node, backgroundImageSource );
71
+ const images = nodes.map( node => {
72
+ if ( node instanceof HTMLImageElement ) {
73
+ return new MeasurableImage( node, imageTagSource, fetchFn );
74
+ } else if ( node instanceof HTMLElement ) {
75
+ if ( ! backgroundImageSource( node ) ) {
76
+ /**
77
+ * Background elements that have no valid URL
78
+ * shouldn't be measured.
79
+ */
80
+ return null;
77
81
  }
78
82
 
79
- return null;
80
- } )
81
- .filter( image => image !== null );
83
+ return new MeasurableImage( node, backgroundImageSource );
84
+ }
85
+
86
+ return null;
87
+ } );
88
+
89
+ return images;
82
90
  }
83
91
 
84
92
  /**