From 67c1eee67a96a1bb1370cdd129716385603b5e67 Mon Sep 17 00:00:00 2001 From: EmilyZhang777 <48967088+EmilyZhang777@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:01:56 -0600 Subject: [PATCH 1/2] Handle EU-Served Images (#1163) --- static/js/formatters-internal.js | 132 +++++++++++++++---- tests/static/js/formatters-internal/image.js | 90 ++++++++++--- 2 files changed, 179 insertions(+), 43 deletions(-) diff --git a/static/js/formatters-internal.js b/static/js/formatters-internal.js index 113e82b79..75474835a 100644 --- a/static/js/formatters-internal.js +++ b/static/js/formatters-internal.js @@ -284,15 +284,15 @@ export function joinList(list, separator) { /** * Given an image object with a url, changes the url to use dynamic thumbnailer and https. - * + * * Note: A dynamic thumbnailer url generated with atLeastAsLarge = true returns an image that is * at least as large in one dimension of the desired size. In other words, the returned image will * be at least as large, and as close as possible to, the largest image that is contained within a * box of the desired size dimensions. - * + * * If atLeastAsLarge = false, the dynamic thumbnailer url will give the largest image that is * smaller than the desired size in both dimensions. - * + * * @param {Object} simpleOrComplexImage An image object with a url * @param {string} desiredSize The desired size of the image ('x') * @param {boolean} atLeastAsLarge Whether the image should be at least as large as the desired @@ -321,13 +321,41 @@ export function image(simpleOrComplexImage = {}, desiredSize = '200x', atLeastAs throw new Error(`Object of type boolean expected. Got ${typeof atLeastAsLarge}.`); } - let desiredWidth, desiredHeight; - let desiredDims = desiredSize.split('x'); + const isEuImage = image.url.includes('eu.mktgcdn.com'); - const [urlWithoutExtension, extension] = _splitStringOnIndex(image.url, image.url.lastIndexOf('.')); + const dynamicUrl = isEuImage + ? _getEuImageDynamicUrl(image, desiredSize, atLeastAsLarge) + : _getUsImageDynamicUrl(image.url, desiredSize, atLeastAsLarge); + + return Object.assign( + {}, + image, + { + url: dynamicUrl.replace('http://', 'https://') + } + ); +} + +/** + * Given a US image url, returns the dynamic url. + * + * @param {string} imageUrl Image's url (e.g. + * 'https://dynl.mktgcdn.com/p/ldMLwj1JkN94-2pwh6CjR_OMy4KnexHJCfZhPAZCbi0/196x400.jpg', + * 'https://a.mktgcdn.com/p/ldMLwj1JkN94-2pwh6CjR_OMy4KnexHJCfZhPAZCbi0/196x400.jpg') + * @param {string} desiredSize The desired size of the image ('x') + * @param {boolean} atLeastAsLarge Whether the image should be at least as large as the desired + * size in one dimension or smaller than the desired size in both + * dimensions. + * @returns {string} A dynamic url (e.g. 'https://dynl.mktgcdn.com/p/ldMLwj1JkN94-2pwh6CjR_OMy4KnexHJCfZhPAZCbi0/200x1.jpg') + */ +function _getUsImageDynamicUrl(imageUrl, desiredSize, atLeastAsLarge) { + const [urlWithoutExtension, extension] = _splitStringOnIndex(imageUrl, imageUrl.lastIndexOf('.')); const [urlBeforeDimensions, dimensions] = _splitStringOnIndex(urlWithoutExtension, urlWithoutExtension.lastIndexOf('/') + 1); const fullSizeDims = dimensions.split('x'); + let desiredWidth, desiredHeight; + let desiredDims = desiredSize.split('x'); + if (desiredDims[0] !== '') { desiredWidth = Number.parseInt(desiredDims[0]); if (Number.isNaN(desiredWidth)) { @@ -348,22 +376,80 @@ export function image(simpleOrComplexImage = {}, desiredSize = '200x', atLeastAs const urlWithDesiredDims = urlBeforeDimensions + desiredWidth + 'x' + desiredHeight + extension; - const dynamicUrl = atLeastAsLarge + return atLeastAsLarge ? _replaceUrlHost(urlWithDesiredDims, 'dynl.mktgcdn.com') : _replaceUrlHost(urlWithDesiredDims, 'dynm.mktgcdn.com'); +} - return Object.assign( - {}, - image, - { - url: dynamicUrl.replace('http://', 'https://') +/** + * Given an EU image url, returns the dynamic url. + * + * @param {Object} image The image object. (e.g. + * { + * url: 'https://a.eu.mktgcdn.com/f/0/FLVfkpR1IwpWrWDuyNYCJWVYIDfPO6x1QSztXozMIzo.jpg', + * sourceUrl: 'https://a.mktgcdn.com/p/UN9RPhz0V9D8bNZ3XfNpkGnAk6ikFhVmgvntlBjVyMA/1200x675.jpg', + * width: 1200, + * height: 675, + * }) + * @param {string} desiredSize The desired size of the image ('x') + * @param {boolean} atLeastAsLarge Whether the image should be at least as large as the desired + * size in one dimension or smaller than the desired size in both + * dimensions. + * @returns {string} A dynamic url (e.g. 'https://dyn.eu.mktgcdn.com/f/0/FLVfkpR1IwpWrWDuyNYCJWVYIDfPO6x1QSztXozMIzo.jpg/width=200,fit=contain') + */ +function _getEuImageDynamicUrl(image, desiredSize, atLeastAsLarge) { + let fullSizeWidth, fullSizeHeight; + if (image.width) { + fullSizeWidth = image.width; + } + if (image.height) { + fullSizeHeight = image.height; + } + + if (image.sourceUrl && (!fullSizeWidth || !fullSizeHeight)) { + const [urlWithoutExtension, _] = _splitStringOnIndex(image.sourceUrl, image.sourceUrl.lastIndexOf('.')); + const [__, dimensions] = _splitStringOnIndex(urlWithoutExtension, urlWithoutExtension.lastIndexOf('/') + 1); + const fullSizeDims = dimensions.split('x'); + + fullSizeWidth = Number.parseInt(fullSizeDims[0]); + fullSizeHeight = Number.parseInt(fullSizeDims[1]); + } + + let desiredDims = desiredSize.split('x'); + let formatOptions = []; + + if (desiredDims[0] !== '') { + const desiredWidth = Number.parseInt(desiredDims[0]); + if (Number.isNaN(desiredWidth)) { + throw new Error("Invalid width specified"); } - ); + + formatOptions.push(`width=${desiredWidth}`); + } else if (!atLeastAsLarge && fullSizeWidth) { + formatOptions.push(`width=${fullSizeWidth}`); + } + + if (desiredDims[1] !== '') { + const desiredHeight = Number.parseInt(desiredDims[1]); + if (Number.isNaN(desiredHeight)) { + throw new Error("Invalid height specified"); + } + + formatOptions.push(`height=${desiredHeight}`); + } else if (!atLeastAsLarge && fullSizeHeight) { + formatOptions.push(`height=${fullSizeHeight}`); + } + + formatOptions.push(`fit=${atLeastAsLarge ? 'cover' : 'contain'}`); + + const urlWithOptions = image.url + `/${formatOptions.join(',')}`; + + return _replaceUrlHost(urlWithOptions, 'dyn.eu.mktgcdn.com'); } /** * Splits a string into two parts at the specified index. - * + * * @param {string} str The string to be split * @param {number} index The index at which to split the string * @returns {Array} The two parts of the string after splitting @@ -374,7 +460,7 @@ function _splitStringOnIndex(str, index) { /** * Replaces the current host of a url with the specified host. - * + * * @param {string} url The url whose host is to be changed * @param {string} host The new host to change to * @returns {string} The url updated with the specified host @@ -519,13 +605,13 @@ export function priceRange(defaultPriceRange, countryCode) { if (countryCode) { const currencySymbol = getSymbolFromCurrency(LocaleCurrency.getCurrency(countryCode)); if (currencySymbol) { - return defaultPriceRange.replace(/\$/g, currencySymbol); + return defaultPriceRange.replace(/\$/g, currencySymbol); } } const { region, language } = parseLocale(_getDocumentLocale()); const currencySymbol = getSymbolFromCurrency(LocaleCurrency.getCurrency(region || language)); if (currencySymbol) { - return defaultPriceRange.replace(/\$/g, currencySymbol); + return defaultPriceRange.replace(/\$/g, currencySymbol); } console.warn('Unable to determine currency symbol from ' + `ISO country code "${countryCode}" or locale "${_getDocumentLocale()}".`); @@ -535,7 +621,7 @@ export function priceRange(defaultPriceRange, countryCode) { /** * Highlights snippets of the provided fieldValue according to the matched substrings. * Each match will be wrapped in tags. - * + * * @param {string} fieldValue The plain, un-highlighted text. * @param {Array} matchedSubstrings The list of matched substrings to * highlight. @@ -543,10 +629,10 @@ export function priceRange(defaultPriceRange, countryCode) { export function highlightField(fieldValue, matchedSubstrings = []) { let highlightedString = ''; - // We must first sort the matchedSubstrings by ascending offset. + // We must first sort the matchedSubstrings by ascending offset. const sortedMatches = matchedSubstrings.slice() .sort((match1, match2) => match1.offset - match2.offset); - + let processedFieldValueIndex = 0; sortedMatches.forEach(match => { const { offset, length } = match; @@ -562,7 +648,7 @@ export function highlightField(fieldValue, matchedSubstrings = []) { * Given an array of youtube videos from the KG, returns an embed link for the first video. * If it is not possible to get an embed link, null is returned instead. * - * @param {Object[]} videos + * @param {Object[]} videos * @returns {string|null} */ export function getYoutubeUrl(videos = []) { @@ -572,7 +658,7 @@ export function getYoutubeUrl(videos = []) { const videoUrl = videos[0]?.video?.url; const youtubeVideoId = videoUrl?.split('watch?v=')[1]; const youtubeVideoUrl = youtubeVideoId - ? 'https://www.youtube.com/embed/' + youtubeVideoId + '?enablejsapi=1' + ? 'https://www.youtube.com/embed/' + youtubeVideoId + '?enablejsapi=1' : null; return youtubeVideoUrl; } @@ -606,7 +692,7 @@ export function getUrlWithTextHighlight(snippet, baseUrl) { /** * construct a list of displayable category names based on given category ids from liveAPI * and a mapping of category ids to names. - * + * * @param {string[]} categoryIds category ids from liveAPI * @param {Object[]} categoryMap mapping of category ids to names * @param {string} categoryMap[].id id of a category entry diff --git a/tests/static/js/formatters-internal/image.js b/tests/static/js/formatters-internal/image.js index c714334d9..b56e7db10 100644 --- a/tests/static/js/formatters-internal/image.js +++ b/tests/static/js/formatters-internal/image.js @@ -1,56 +1,106 @@ import Formatters from 'static/js/formatters.js'; describe('image formatter', () => { - const img = { - url: 'https://a.mktgcdn.com/p/1024x768.jpg' + const usUrl = 'https://a.mktgcdn.com/p/1024x768.jpg'; + const euUrl = 'https://dyn.eu.mktgcdn.com/f/0/FOO.jpg'; + const usImg = {url: usUrl}; + const euImg = { + url: euUrl, + width: 1024, + height: 768, + sourceUrl: 'https://a.mktgcdn.com/p/FOO/1024x768.jpg', } describe('when choosing the smallest image over threshold', () => { it('By default chooses the smallest image with width >= 200', () => { - const imageUrl = Formatters.image(img).url; - expect(imageUrl).toEqual('https://dynl.mktgcdn.com/p/200x1.jpg'); + const usImageUrl = Formatters.image(usImg).url; + expect(usImageUrl).toEqual('https://dynl.mktgcdn.com/p/200x1.jpg'); + const euImageUrl = Formatters.image(euImg).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=200,fit=cover'); }); it('Can restrict the dimensions by width', () => { - const imageUrl = Formatters.image(img, '601x').url; - expect(imageUrl).toEqual('https://dynl.mktgcdn.com/p/601x1.jpg'); + const usImageUrl = Formatters.image(usImg, '601x').url; + expect(usImageUrl).toEqual('https://dynl.mktgcdn.com/p/601x1.jpg'); + const euImageUrl = Formatters.image(euImg, '601x').url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=601,fit=cover'); }); it('Can restrict the dimensions by height', () => { - const imageUrl = Formatters.image(img, 'x338').url; - expect(imageUrl).toEqual('https://dynl.mktgcdn.com/p/1x338.jpg'); + const usImageUrl = Formatters.image(usImg, 'x338').url; + expect(usImageUrl).toEqual('https://dynl.mktgcdn.com/p/1x338.jpg'); + const euImageUrl = Formatters.image(euImg, 'x338').url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/height=338,fit=cover'); }); it('Can restrict by both dimensions', () => { - const imageUrl = Formatters.image(img, '601x338').url; - expect(imageUrl).toEqual('https://dynl.mktgcdn.com/p/601x338.jpg'); + const usImageUrl = Formatters.image(usImg, '601x338').url; + expect(usImageUrl).toEqual('https://dynl.mktgcdn.com/p/601x338.jpg'); + const euImageUrl = Formatters.image(euImg, '601x338').url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=601,height=338,fit=cover'); }); it('returns the smallest image when no dimensions given', () => { - const imageUrl = Formatters.image(img, 'x').url; - expect(imageUrl).toEqual('https://dynl.mktgcdn.com/p/1x1.jpg'); + const usImageUrl = Formatters.image(usImg, 'x').url; + expect(usImageUrl).toEqual('https://dynl.mktgcdn.com/p/1x1.jpg'); + const euImageUrl = Formatters.image(euImg, 'x').url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/fit=cover'); }); }); describe('when choosing the biggest image under threshold', () => { it('Can restrict the dimensions by width', () => { - const imageUrl = Formatters.image(img, '601x', false).url; - expect(imageUrl).toEqual('https://dynm.mktgcdn.com/p/601x768.jpg'); + const usImageUrl = Formatters.image(usImg, '601x', false).url; + expect(usImageUrl).toEqual('https://dynm.mktgcdn.com/p/601x768.jpg'); + const euImageUrl = Formatters.image(euImg, '601x', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=601,height=768,fit=contain'); }); it('Can restrict the dimensions by height', () => { - const imageUrl = Formatters.image(img, 'x338', false).url; - expect(imageUrl).toEqual('https://dynm.mktgcdn.com/p/1024x338.jpg'); + const usImageUrl = Formatters.image(usImg, 'x338', false).url; + expect(usImageUrl).toEqual('https://dynm.mktgcdn.com/p/1024x338.jpg'); + const euImageUrl = Formatters.image(euImg, 'x338', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=1024,height=338,fit=contain'); }); it('Can restrict by both dimensions', () => { - const imageUrl = Formatters.image(img, '999x338', false).url; - expect(imageUrl).toEqual('https://dynm.mktgcdn.com/p/999x338.jpg'); + const usImageUrl = Formatters.image(usImg, '999x338', false).url; + expect(usImageUrl).toEqual('https://dynm.mktgcdn.com/p/999x338.jpg'); + const euImageUrl = Formatters.image(euImg, '999x338', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=999,height=338,fit=contain'); }); it('return the largest image when no dimensions given', () => { - const imageUrl = Formatters.image(img, 'x', false).url; - expect(imageUrl).toEqual('https://dynm.mktgcdn.com/p/1024x768.jpg'); + const usImageUrl = Formatters.image(usImg, 'x', false).url; + expect(usImageUrl).toEqual('https://dynm.mktgcdn.com/p/1024x768.jpg'); + const euImageUrl = Formatters.image(euImg, 'x', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=1024,height=768,fit=contain'); + }); + }); + + describe('when image is served from EU with no dimensions specified', () => { + it('when choosing the biggest image under threshold, use the width and height on the image object if exists', () => { + const euImageUrl = Formatters.image( + { url: euUrl, + width: 1024, + height: 768, + sourceUrl: 'https://a.mktgcdn.com/p/FOO/516x384.jpg', + }, 'x', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=1024,height=768,fit=contain'); + }); + + it('when choosing the biggest image under threshold, use dimensions from the sourceUrl if width/height does not exist', () => { + const euImageUrl = Formatters.image( + { url: euUrl, + width: 1024, + sourceUrl: 'https://a.mktgcdn.com/p/FOO/516x384.jpg' + }, 'x', false).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/width=516,height=384,fit=contain'); + }); + + it('when choosing the smallest image over threshold, omit width/height if can\'t parse it from the image object', () => { + const euImageUrl = Formatters.image({url: euUrl}, 'x', true).url; + expect(euImageUrl).toEqual('https://dyn.eu.mktgcdn.com/f/0/FOO.jpg/fit=cover'); }); }); }); \ No newline at end of file From 053f7415ac5b54bba5095b693698f385f3686e85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:21:54 -0500 Subject: [PATCH 2/2] Update Package Version to v1.33.4 (#1164) An automated PR which updates the version number in package.json and package-lock.json files --- package-lock.json | 4 ++-- package.json | 2 +- static/package-lock.json | 4 ++-- static/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f58b37bb..e323182c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "devDependencies": { "@axe-core/puppeteer": "^4.5.2", "@babel/core": "^7.9.6", diff --git a/package.json b/package.json index 4e8a10895..dcda12cd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "description": "A starter Search theme for hitchhikers", "keywords": [ "jambo", diff --git a/static/package-lock.json b/static/package-lock.json index c8c3a3045..c2bff1b8b 100644 --- a/static/package-lock.json +++ b/static/package-lock.json @@ -1,12 +1,12 @@ { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "license": "BSD-3-Clause", "dependencies": { "@vimeo/player": "^2.15.3", diff --git a/static/package.json b/static/package.json index 6b7972664..0996cb943 100644 --- a/static/package.json +++ b/static/package.json @@ -1,6 +1,6 @@ { "name": "answers-hitchhiker-theme", - "version": "1.33.3", + "version": "1.33.4", "description": "Toolchain for use with the HH Theme", "main": "Gruntfile.js", "scripts": {