PageSourceSearch

https://www.bancomundial.org/ext/scripts/utils-core.js

js bancomundial.org collected 2026-09-24 08:27:10 UTC 13,218 bytes, 460 lines download raw bytes

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.