Setting the file. One moment.
Wp Image URL · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page 39.41
PLUGIN READ PLANS
Position
36 of 92 lib/ wp-image-url.js
JavaScript · 196 lines · 9 KB
// image-optimizer plugin is free to redefine
15 //
16 // So no caller may name a field. Every caller hands the whole reference to resolveImageUrl and
17 // gets back the biggest file available. Doing it any other way is how a whole catalog shipped as
18 // 150x150 thumbnails.
19 //
20 // It matters because the destination gives exactly one attempt: Wix Stores media takes ONE url
21 // per item and generates every derivative from it, with no way to raise the quality afterwards.
22 // Whatever this function returns is that product image's permanent ceiling.
23
24 // WordPress names every crop it generates by appending the dimensions to the filename, so a
25 // candidate WITHOUT that suffix is the uploaded original. This is a filename convention of the
26 // source platform, not a guarantee — do not reuse this module for a non-WordPress source
27 // without re-deriving the rule.
28 //
29 // The extension group repeats (`(\.ext)+`) because image optimizers append their own format
30 // rather than replacing the original one: WebP Express, EWWW and Imagify all emit
31 // `photo-150x150.jpg.webp` beside `photo-150x150.jpg`. Matching only the LAST extension made
32 // every such crop look like an uploaded original, so rule 3 below returned the first one it saw —
33 // a 150px thumbnail winning over a 1200px sibling, reported as `isCrop: false` so nothing flagged
34 // it either. That is unrecoverable once ingested, which is the whole reason this module exists.
35 const CROP_SUFFIX = /-( \d + )x( \d + )( \. [A-Za-z0-9] + ) +$ / ;
36
37 // Fields that hold a single URL, across every WordPress-family shape we read:
38 // wc/v3 + wc/store/v1 product images -> src, thumbnail
39 // wp/v2 media -> source_url
40 const SINGLE_URL_FIELDS = [ 'src' , 'thumbnail' , 'source_url' ];
41
42 function isNonEmptyString ( value ) {
43 return typeof value === 'string' && value. trim () !== '' ;
44 }
45
46 function pathOf ( url ) {
47 // Compare against the path only. A query string can end in anything, and matching the crop
48 // suffix against it would misread `?v=-150x150` as a crop.
49 const withoutFragment = String (url). split ( '#' )[ 0 ];
50 return withoutFragment. split ( '?' )[ 0 ];
51 }
52
53 // Width descriptors in a srcset entry ("<url> 800w") and the width baked into a crop filename
54 // are both ranking keys and nothing else. Never treat either as the image's true dimensions.
55 function declaredWidth ( url , descriptorWidth ) {
56 const match = CROP_SUFFIX . exec ( pathOf (url));
57 const fromName = match ? Number (match[ 1 ]) : 0 ;
58 return Math. max ( Number (descriptorWidth) || 0 , fromName);
59 }
60
61 function parseSrcset ( srcset ) {
62 if ( ! isNonEmptyString (srcset)) return [];
63 return srcset
64 . split ( ',' )
65 . map (( entry ) => entry. trim ())
66 . filter (Boolean)
67 . map (( entry ) => {
68 const parts = entry. split ( / \s + / );
69 const url = parts[ 0 ];
70 const descriptor = parts. length > 1 ? parts[parts. length - 1 ] : '' ;
71 const width = / ^ \d + w $ / . test (descriptor) ? Number (descriptor. slice ( 0 , - 1 )) : 0 ;
72 return isNonEmptyString (url) ? { url, width } : null ;
73 })
74 . filter (Boolean);
75 }
76
77 // Every URL the record offers for this one picture, each with whatever width we can infer.
78 function collectCandidates ( record ) {
79 if ( ! record || typeof record !== 'object' ) return [];
80 const candidates = [];
81
82 for ( const field of SINGLE_URL_FIELDS ) {
83 if ( isNonEmptyString (record[field])) candidates. push ({ url: record[field], width: 0 });
84 }
85
86 // wc/store/v1 exposes a second srcset for the thumbnail rendition; it occasionally carries a
87 // width the main one does not, and taking the max across both costs nothing.
88 for ( const field of [ 'srcset' , 'thumbnail_srcset' ]) {
89 candidates. push ( ... parseSrcset (record[field]));
90 }
91
92 // wp/v2 media keeps its renditions in a nested map rather than a srcset string.
93 const sizes = record.media_details && record.media_details.sizes;
94 if (sizes && typeof sizes === 'object' ) {
95 for ( const size of Object. values (sizes)) {
96 if (size && isNonEmptyString (size.source_url)) {
97 candidates. push ({ url: size.source_url, width: Number (size.width) || 0 });
98 }
99 }
100 }
101
102 return candidates;
103 }
104
105 // A raw URL with non-ASCII characters is not fetchable by ordinary HTTP clients — our own
106 // verification tooling throws on it before the destination ever sees the request. Encode the
107 // path only: the host is already punycode or ASCII, and the query is left to the URL parser's
108 // own normalization so its parameter structure is never rewritten.
109 function encodeImageUrl ( url ) {
110 if ( ! isNonEmptyString (url)) return url;
111 try {
112 const parsed = new URL (url);
113 parsed.pathname = parsed.pathname
114 . split ( '/' )
115 . map (( segment ) => encodeURIComponent ( decodeURIComponent (segment)))
116 . join ( '/' );
117 return parsed. toString ();
118 } catch {
119 // Not an absolute URL. Hand it back untouched rather than mangling a relative path — the
120 // caller knows its own base and this module does not.
121 return url;
122 }
123 }
124
125 // Whether two references name the same file. Post HTML may carry a percent-encoded path while
126 // the media record's `source_url` carries the decoded (e.g. non-Latin) filename, or the reverse;
127 // both reduce to one canonical form through encodeImageUrl. Never a fuzzy match: scheme, host,
128 // every path segment and the query must agree after that canonicalization.
129 function sameImageUrl ( a , b ) {
130 if ( ! isNonEmptyString (a) || ! isNonEmptyString (b)) return false ;
131 return encodeImageUrl (a) === encodeImageUrl (b);
132 }
133
134 // The one entry point. Returns the encoded URL of the largest file the record offers, or null
135 // when it offers none. Never invents a URL: a record with no image is a fact to report, not a
136 // gap to fill with a placeholder.
137 function resolveImageUrl ( record ) {
138 const candidates = collectCandidates (record);
139 if (candidates. length === 0 ) return null ;
140
141 const original = candidates. find (( candidate ) => ! CROP_SUFFIX . test ( pathOf (candidate.url)));
142 if (original) return encodeImageUrl (original.url);
143
144 // Only crops exist. Take the widest rather than failing — something always beats a thumbnail —
145 // and let the caller report it via resolvedIsCrop().
146 let best = candidates[ 0 ];
147 let bestWidth = declaredWidth (best.url, best.width);
148 for ( const candidate of candidates. slice ( 1 )) {
149 const width = declaredWidth (candidate.url, candidate.width);
150 if (width > bestWidth) {
151 best = candidate;
152 bestWidth = width;
153 }
154 }
155 return encodeImageUrl (best.url);
156 }
157
158 // True when the best available URL is still a generated crop, i.e. the source genuinely had no
159 // original to give. Callers MUST surface this: a migration may ship a small picture when the
160 // source only has a small picture, but it may not do so silently.
161 function resolvedIsCrop ( url ) {
162 return isNonEmptyString (url) && CROP_SUFFIX . test ( pathOf (url));
163 }
164
165 // Resolve a whole images[] array in order, dropping references that carry no URL at all.
166 // Order is load-bearing downstream: the first surviving item becomes the product's main media.
167 function resolveImageUrls ( records ) {
168 if ( ! Array. isArray (records)) return [];
169 return records
170 . map (( record ) => {
171 const url = resolveImageUrl (record);
172 if ( ! url) return null ;
173 // The attachment id is what joins this product reference to wp/v2's media
174 // record. Preserve an explicit zero for an absent/invalid id so the confirmation boundary
175 // can fail closed with `no-source-id` rather than guessing from a filename.
176 const sourceId = Number (record && record.id);
177 const resolved = {
178 url,
179 sourceId: Number. isInteger (sourceId) && sourceId > 0 ? sourceId : 0 ,
180 isCrop: resolvedIsCrop (url),
181 };
182 if ( isNonEmptyString (record && record.alt)) resolved.altText = record.alt;
183 if ( isNonEmptyString (record && record.alt_text)) resolved.altText = record.alt_text;
184 if ( isNonEmptyString (record && record.name)) resolved.displayName = record.name;
185 return resolved;
186 })
187 . filter (Boolean);
188 }
189
190 module . exports = {
191 resolveImageUrl,
192 resolveImageUrls,
193 resolvedIsCrop,
194 encodeImageUrl,
195 sameImageUrl,
196 };