1/** 2 * utils-core.js â Lightweight utilities needed at page load / eager phase. 3 * Kept small (~8KB) so scripts.js only parses this on initial load. 4 * utils.js re-exports everything from here for backward compat with blocks. 5 */ 6import { getMetadata, fetchPlaceholders, loadScript } from './aem.js'; 7 8export const PATH_PREFIX = '/ext'; 9export const TAG_ROOT = 'world-bank:'; 10export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ru', 'fr', 'es', 'ar', 'ja']; 11export const RTL_LANGUAGES = ['ar']; // list of RTL Languages 12export const INTERNAL_PAGES = ['/footer', '/nav', '/fragments', '/data', '/drafts']; 13export const DEFAULT_DOMAIN = 'https://www.worldbank.org'; 14 15let lang; 16 17export function isHomePage() { 18 return /^\/ext\/[a-z]{2}\/home\/?$/.test(window.location.pathname); 19} 20 21export function getTags(tagType) { 22 const tags = getMetadata("cq-tags"); 23 // const tags = "wbg:subject/research development and innovation, subject:economic growth, subject:governance in public sector, subject:poverty measurement and analysis, subject:sustainable-development-goals, subject:sustainable economic growth"; 24 if (!tags) return ""; 25 return tags 26 // Split on commas only when followed by a tag prefix (e.g., "world-bank:", "wbg:") 27 // This preserves commas within tag values like "Congo, Democratic Republic of" 28 .split(/,\s*(?=[\w-]+:)/) 29 ?.map((tag) => tag?.trim()) 30 ?.filter((tag) => { 31 // Handle both formats: 32 // 1. Old format: "country:value" or "region:value" 33 // 2. New format: "world-bank:country/value" or "world-bank:regions/value" 34 return tag?.startsWith(`${tagType}:`) || tag?.includes(`:${tagType}/`); 35 }) 36 ?.map((tag) => { 37 // Extract value from either format 38 const prefix = `${tagType}/` 39 if (tag.includes(prefix)) { 40 // New format: "world-bank:country/Cameroon" -> "Cameroon" 41 return tag.substring(tag.lastIndexOf(prefix) + prefix.length).trim(); 42 } else { 43 // Old format: "country:Cameroon" -> "Cameroon" 44 return tag?.replace(`${tagType}:`, "").trim(); 45 } 46 }) 47 ?.join(","); 48} 49 50/** 51 * Process current pathname and return details for use in language switching 52 * Considers pathnames like /en/path/to/content and 53 * /content/world-bank/global/en/path/to/content.html for both EDS and AEM 54 */ 55export function getPathDetails() { 56 const { pathname } = window.location; 57 const isContentPath = pathname.startsWith('/content'); 58 const parts = pathname.split('/'); 59 // const safeLangGet = (index) => (parts.length > index ? parts[index] : 'en'); 60 /* 5 is the index of the language in the path for AEM content paths like 61 /content/world-bank/corporate/ext/en/path/to/content.html 62 2 is the index of the language in the path for EDS paths like /en/path/to/content 63 */ 64 // let langCode = isContentPath ? safeLangGet(5) : safeLangGet(2); 65 let langCode = ''; 66 for (const part of parts) { 67 if (part && part.length === 2) { 68 langCode = part; 69 break; 70 } 71 } 72 // remove suffix from lang if any 73 if (langCode.indexOf('.') > -1) { 74 langCode = langCode.substring(0, langCode.indexOf('.')); 75 } 76 if (!langCode) langCode = 'en'; // default to en 77 // substring before lang 78 const prefix = pathname.substring(0, pathname.indexOf(`/${langCode}`)) || ''; 79 const suffix = pathname.substring(pathname.indexOf(`/${langCode}`) + langCode.length + 1) || ''; 80 return { 81 prefix, 82 suffix, 83 langCode, 84 isContentPath, 85 }; 86} 87 88/** 89 * Fetch and return language of current page. 90 * @returns language of current page 91 */ 92export function getLanguage() { 93 lang = getPathDetails().langCode; 94 if (!lang) { 95 lang = 'en'; 96 } 97 return lang; 98} 99 100export function setPageLanguage() { 101 const currentLang = getLanguage(); 102 document.documentElement.lang = currentLang; 103 if (RTL_LANGUAGES.includes(currentLang)) { 104 document.documentElement.dir = 'rtl'; 105 } 106} 107 108/** 109 * Remove prefix from tag 110 * @param {*} tag 111 * @returns 112 */ 113export function processTags(tag, prefix = '') { 114 if (tag) { 115 return tag.replace(TAG_ROOT, '').replace(`${prefix}/`, ''); 116 } 117 return null; 118} 119 120/** 121 * Get page context (country/region/topic) from URL path and extract corresponding value from cq-tags. 122 * URL patterns analyzed: 123 * - /ext/en/country/cameroon -> reads world-bank:country/Cameroon -> returns 'Cameroon' 124 * - /ext/en/region/mena -> reads world-bank:regions/Africa -> returns 'Africa' 125 * - /ext/en/topic/education -> reads world-bank:topic/Education -> returns 'Education' 126 * 127 * @returns {object|null} Object with { type, value } or null if not a contextual page 128 * @example 129 * // For URL: /ext/en/region/mena with meta tag world-bank:regions/Africa 130 * getPageContext() // { type: 'region', value: 'Africa' } 131 */ 132export function getPageContext() { 133 const pathname = window.location.pathname;
134 const parts = pathname.split('/').filter(Boolean); 135 136 // Expected structure: [ext, lang, type, code, ...] 137 // e.g., /ext/en/country/cameroon or /ext/en/region/mena 138 if (parts.length < 3) { 139 return null; 140 } 141 142 // Use getLanguage() to reliably detect the language code 143 const langCode = getLanguage(); 144 145 // Find the index of the language code in the path 146 const langIndex = parts.indexOf(langCode); 147 148 if (langIndex === -1 || langIndex + 1 >= parts.length) { 149 return null; 150 } 151 152 const typeIndex = langIndex + 1; 153 const type = parts[typeIndex]?.toLowerCase(); 154 155 // Validate type is one of: country, region, topic 156 const validTypes = ['country', 'region', 'topic']; 157 if (!validTypes.includes(type)) { 158 return null; 159 } 160 161 // Map URL type to cq-tags prefix 162 const tagTypeMap = { 163 'country': 'country', 164 'region': 'regions', // Note: plural in cq-tags 165 'topic': 'topic' 166 }; 167 168 const tagType = tagTypeMap[type]; 169 170 // Extract value from cq-tags metadata ONLY (not from URL) 171 const value = getTags(tagType); 172 173 if (!value) { 174 // eslint-disable-next-line no-console 175 console.warn(`[getPageContext] No cq-tags found for type: ${tagType}`); 176 return { 177 type, 178 value: null 179 }; 180 } 181 182 return { 183 type, // 'country', 'region', or 'topic' (from URL) 184 value // Actual value from cq-tags like 'Cameroon', 'Africa', 'Education' 185 }; 186} 187 188/** 189 * Returns the true of the current page in the browser. 190 * If the page is running in a iframe with srcdoc, 191 * the ancestor origin + the path query param is returned. 192 * @returns {String} The href of the current page or the href of the block running in the library 193 */ 194export function getHref() { 195 if (window.location.href !== 'about:srcdoc') return window.location.href; 196 197 const urlParams = new URLSearchParams(window.parent.location.search); 198 return `${window.parent.location.origin}${urlParams.get('path')}`; 199} 200 201/** 202 * Check if a page is internal or not 203 */ 204export function isInternalPage() { 205 const pageUrl = getHref(); 206 // eslint-disable-next-line consistent-return 207 INTERNAL_PAGES.forEach((element) => { if (pageUrl.indexOf(element) > 0) return true; }); 208 return false; 209} 210 211/** 212 * Only a bot score of 1-29 from the wbg_ctx cookie indicates a bot; 213 * everything else (including a missing cookie) is treated as human. 214 */ 215export function isHuman() { 216 const match = document.cookie.match(/(?:^|;\s*)wbg_ctx=([^;]*)/); 217 if (!match) return true; 218 const score = Number(decodeURIComponent(match[1])); 219 if (!Number.isFinite(score)) return true; 220 return !(score >= 1 && score <= 29); 221} 222 223export async function loadAdobeAnalytics() { 224 if (!scriptEnabled() || isInternalPage() || !isHuman()) { return; } 225 const config = await fetchPlaceholders(PATH_PREFIX); 226 await loadScript(config.analyticsEndpoint); 227} 228 229export function formatDate(dObjStr) { 230 if (dObjStr) { 231 const dObj = new Date(dObjStr); 232 const yyyy = dObj.getFullYear(); 233 let mm = dObj.getMonth() + 1; 234 let dd = dObj.getDate(); 235 236 if (dd < 10) dd = `0${dd}`; 237 if (mm < 10) mm = `0${mm}`; 238 239 const formatted = `${mm}-${dd}-${yyyy}`; 240 return formatted; 241 } 242 return ''; 243} 244 245/** 246 * Gets the extension of a URL. 247 * @param {string} url The URL 248 * @returns {string} The extension 249 * @private 250 * @example 251 * get_url_extension('https://example.com/foo.jpg'); 252 * // returns 'jpg' 253 * get_url_extension('https://example.com/foo.jpg?bar=baz'); 254 * // returns 'jpg' 255 * get_url_extension('https://example.com/foo'); 256 * // returns '' 257 * get_url_extension('https://example.com/foo.jpg#qux'); 258 * // returns 'jpg' 259 */ 260export function getUrlExtension(url) { 261 return url.split(/[#?]/)[0].split('.').pop().trim(); 262} 263 264/** 265 * Get the query string value 266 * @param {*} key 267 * @returns 268 */ 269export function getQueryString(key = 'tip', path = window.location.href) { 270 const pageUrl = new URL(path); 271 return pageUrl.searchParams.get(key); 272} 273 274/** 275 * Check if 3rd party scripts are enabled or not. 276 * @returns 277 */ 278export function scriptEnabled() { 279 if (getQueryString('tip') === 'noscript') return false; 280 281 return true; 282} 283 284/** 285 * 286 * Helper function to create a <source> element 287 * 288 * @returns imageSource 289 */ 290export function createSource(src, width, mediaQuery) { 291 const { pathname } = new URL(src, window.location.href); 292 const source = document.createElement('source'); 293 source.type = 'image/webp'; 294 source.srcset = `${pathname}?width=${width}&format=webply&optimize=medium`; 295 source.media = mediaQuery; 296 297 return source; 298} 299 300export function isAuthoring() { 301 return window.location.hostname.includes('author'); 302} 303 304/** 305 * Detects if the user is actively using the Universal Editor canvas. 306 */ 307export function isInsideEditor() { 308 if (!isAuthoring()) return false;
309 310 try { 311 // Check if we are in an iframe and if the parent URL contains '/canvas/' 312 return window.self !== window.top && window.parent.location.href.includes('/canvas/'); 313 } catch (e) { 314 // Fallback if parent access is blocked 315 return false; 316 } 317} 318 319export async function fetchData(url, method = 'GET', headers = {}, body = null) { 320 try { 321 const options = { method, headers: { ...headers } }; 322 if (method === 'POST' && body) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(body); } 323 const response = await fetch(url, options); 324 if (!response.ok) { 325 throw new Error(`HTTP error! status: ${response.status}`); 326 } 327 return await response.json(); 328 } catch (error) { 329 // eslint-disable-next-line no-console 330 console.error(`Error fetching data from ${url}:`, error); 331 return null; 332 } 333} 334 335export function isJobsPage() { 336 const header = getMetadata('header'); 337 if (header && header === 'jobs') { 338 return true; 339 } 340 return false; 341} 342 343export function isAgriConnectPage(params) { 344 const header = getMetadata('header'); 345 if (header && header === 'agriconnect') { 346 return true; 347 } 348 return false; 349} 350 351export async function decorateImagesWithParams( 352 image, 353 titleText, 354 altText, 355 aspectRatioDiv, 356 objectPosDiv, 357 loadingDiv 358) { 359 const getText = (el, defaultValue = '') => { 360 if (el instanceof Element) { 361 const p = el.querySelector('p'); 362 return p ? p.textContent.trim() : defaultValue; 363 } 364 return defaultValue; 365 }; 366 367 const aspectRatio = getText(aspectRatioDiv); 368 const objectPos = getText(objectPosDiv); 369 370 const pictureTag = image.querySelector('picture'); 371 if (!pictureTag) return; 372 373 const imgEl = pictureTag.querySelector('img'); 374 if (!imgEl) return; 375 376 const src = imgEl.src || ''; 377 const fileName = src.substring(src.lastIndexOf('/') + 1); 378 const fallbackAlt = fileName.includes('.') 379 ? fileName.substring(0, fileName.lastIndexOf('.')) 380 : fileName; 381 382 const finalAlt = 383 altText?.trim() || 384 imgEl.getAttribute('alt')?.trim() || 385 fallbackAlt; 386 387 const finalTitle = 388 titleText?.trim() || 389 imgEl.getAttribute('title')?.trim() || 390 fallbackAlt; 391 392 imgEl.setAttribute('alt', finalAlt); 393 imgEl.setAttribute('title', finalTitle); 394 395 if (aspectRatio) { 396 imgEl.style.aspectRatio = aspectRatio; 397 } 398 399 if (objectPos) { 400 imgEl.style.objectPosition = objectPos; 401 } 402 403 // Only override loading if author explicitly set a value; 404 // otherwise preserve the existing attribute (AEM boilerplate sets 'eager' on LCP images). 405 const authorLoading = getText(loadingDiv); 406 if (authorLoading) { 407 imgEl.loading = authorLoading; 408 } 409 410 [aspectRatioDiv, objectPosDiv, loadingDiv].forEach((el) => { 411 if (el instanceof Element) el.remove(); 412 }); 413} 414 415// Image with parameters 416export async function getFinalImageWithParams( 417 overrideImage, 418 assetDiv, 419 titleText, 420 altText, 421 aspectRatioDiv, 422 objectPosDiv, 423 loadingValueDiv 424) { 425 let finalProfileImage; 426 427 const hasOverrideImage = 428 overrideImage && overrideImage.querySelector('img'); 429 430 if (hasOverrideImage) { 431 finalProfileImage = overrideImage; 432 433 if (assetDiv) { 434 assetDiv.remove(); 435 } 436 } else { 437 finalProfileImage = assetDiv; 438 } 439 440 const getText = (el) => el?.textContent?.trim() || ''; 441 442 const hasParams = 443 (altText && altText.trim() !== '') || 444 getText(aspectRatioDiv) || 445 getText(objectPosDiv) || 446 getText(loadingValueDiv); 447 448 if (hasParams && finalProfileImage) { 449 await decorateImagesWithParams( 450 finalProfileImage, 451 titleText, 452 altText, 453 aspectRatioDiv, 454 objectPosDiv, 455 loadingValueDiv 456 ); 457 } 458 459 return finalProfileImage; 460}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.