/* * Copyright 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ /* eslint-env browser */ function sampleRUM(checkpoint, data) { // eslint-disable-next-line max-len const timeShift = () => (window.performance ? window.performance.now() : Date.now() - window.hlx.rum.firstReadTime); try { window.hlx = window.hlx || {}; if (!window.hlx.rum || !window.hlx.rum.collector) { sampleRUM.enhance = () => {}; const params = new URLSearchParams(window.location.search); const { currentScript } = document; const rate = params.get('rum') || window.SAMPLE_PAGEVIEWS_AT_RATE || params.get('optel') || (currentScript && currentScript.dataset.rate); const rateValue = { on: 1, off: 0, high: 10, low: 1000, }[rate]; const weight = rateValue !== undefined ? rateValue : 100; const id = (window.hlx.rum && window.hlx.rum.id) || crypto.randomUUID().slice(-9); const isSelected = (window.hlx.rum && window.hlx.rum.isSelected) || (weight > 0 && Math.random() * weight < 1); // eslint-disable-next-line object-curly-newline, max-len window.hlx.rum = { weight, id, isSelected, firstReadTime: window.performance ? window.performance.timeOrigin : Date.now(), sampleRUM, queue: [], collector: (...args) => window.hlx.rum.queue.push(args), }; if (isSelected) { const dataFromErrorObj = (error) => { const errData = { source: 'undefined error' }; try { errData.target = error.toString(); if (error.stack) { errData.source = error.stack .split('\n') .filter((line) => line.match(/https?:\/\//)) .shift() .replace(/at ([^ ]+) \((.+)\)/, '$1@$2') .replace(/ at /, '@') .trim(); } } catch (err) { /* error structure was not as expected */ } return errData; }; window.addEventListener('error', ({ error }) => { const errData = dataFromErrorObj(error); sampleRUM('error', errData); }); window.addEventListener('unhandledrejection', ({ reason }) => { let errData = { source: 'Unhandled Rejection', target: reason || 'Unknown', }; if (reason instanceof Error) { errData = dataFromErrorObj(reason); } sampleRUM('error', errData); }); window.addEventListener('securitypolicyviolation', (e) => { if (e.blockedURI.includes('helix-rum-enhancer') && e.disposition === 'enforce') { const errData = { source: 'csp', target: e.blockedURI, }; sampleRUM.sendPing('error', timeShift(), errData); } }); sampleRUM.baseURL = sampleRUM.baseURL || new URL(window.RUM_BASE || '/', new URL('https://ot.aem.live')); sampleRUM.collectBaseURL = sampleRUM.collectBaseURL || sampleRUM.baseURL; sampleRUM.sendPing = (ck, time, pingData = {}) => { // eslint-disable-next-line max-len, object-curly-newline const rumData = JSON.stringify({ weight, id, referer: window.location.href, checkpoint: ck, t: time, ...pingData, }); const urlParams = window.RUM_PARAMS ? new URLSearchParams(window.RUM_PARAMS).toString() || '' : ''; const { href: url, origin } = new URL( `.rum/${weight}${urlParams ? `?${urlParams}` : ''}`, sampleRUM.collectBaseURL, ); const body = origin === window.location.origin ? new Blob([rumData], { type: 'application/json' }) : rumData; navigator.sendBeacon(url, body); // eslint-disable-next-line no-console console.debug(`ping:${ck}`, pingData); }; sampleRUM.sendPing('top', timeShift()); sampleRUM.enhance = () => { // only enhance once if (document.querySelector('script[src*="rum-enhancer"]')) return; const { enhancerVersion, enhancerHash } = sampleRUM.enhancerContext || {}; const script = document.createElement('script'); if (enhancerHash) { script.integrity = enhancerHash; script.setAttribute('crossorigin', 'anonymous'); } script.src = new URL( `.rum/@adobe/helix-rum-enhancer@${enhancerVersion || '^2'}/src/index.js`, sampleRUM.baseURL, ).href; document.head.appendChild(script); }; if (!window.hlx.RUM_MANUAL_ENHANCE) { sampleRUM.enhance(); } } } if (window.hlx.rum && window.hlx.rum.isSelected && checkpoint) { window.hlx.rum.collector(checkpoint, data, timeShift()); } document.dispatchEvent(new CustomEvent('rum', { detail: { checkpoint, data } })); } catch (error) { // something went awry } } /** * Setup block utils. */ function setup() { window.hlx = window.hlx || {}; window.hlx.RUM_MASK_URL = 'full'; window.hlx.RUM_MANUAL_ENHANCE = true; window.hlx.codeBasePath = ''; window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); if (scriptEl) { try { const scriptURL = new URL(scriptEl.src, window.location); if (scriptURL.host === window.location.host) { [window.hlx.codeBasePath] = scriptURL.pathname.split('/scripts/scripts.js'); } else { [window.hlx.codeBasePath] = scriptURL.href.split('/scripts/scripts.js'); } } catch (error) { // eslint-disable-next-line no-console console.log(error); } } } /** * Auto initialization. */ function init() { setup(); sampleRUM.collectBaseURL = window.origin; sampleRUM(); } /** * Sanitizes a string for use as class name. * @param {string} name The unsanitized string * @returns {string} The class name */ function toClassName(name) { return typeof name === 'string' ? name .toLowerCase() .replace(/[^0-9a-z]/gi, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') : ''; } /** * Sanitizes a string for use as a js property name. * @param {string} name The unsanitized string * @returns {string} The camelCased name */ function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } /** * Extracts the config from a block. * @param {Element} block The block element * @returns {object} The block config */ // eslint-disable-next-line import/prefer-default-export function readBlockConfig(block) { const config = {}; block.querySelectorAll(':scope > div').forEach((row) => { if (row.children) { const cols = [...row.children]; if (cols[1]) { const col = cols[1]; const name = toClassName(cols[0].textContent); let value = ''; if (col.querySelector('a')) { const as = [...col.querySelectorAll('a')]; if (as.length === 1) { value = as[0].href; } else { value = as.map((a) => a.href); } } else if (col.querySelector('img')) { const imgs = [...col.querySelectorAll('img')]; if (imgs.length === 1) { value = imgs[0].src; } else { value = imgs.map((img) => img.src); } } else if (col.querySelector('p')) { const ps = [...col.querySelectorAll('p')]; if (ps.length === 1) { value = ps[0].textContent; } else { value = ps.map((p) => p.textContent); } } else value = row.children[1].textContent; config[name] = value; } } }); return config; } /** * Loads a CSS file. * @param {string} href URL to the CSS file */ async function loadCSS(href) { return new Promise((resolve, reject) => { if (!document.querySelector(`head > link[href="proxy.php?url=https%3A%2F%2Fwww.cloud.com%2F%24%7Bhref%7D"]`)) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; link.onload = resolve; link.onerror = reject; document.head.append(link); } else { resolve(); } }); } /** * Loads a non module JS file. * @param {string} src URL to the JS file * @param {Object} attrs additional optional attributes */ async function loadScript(src, attrs) { return new Promise((resolve, reject) => { if (!document.querySelector(`head > script[src="proxy.php?url=https%3A%2F%2Fwww.cloud.com%2F%24%7Bsrc%7D"]`)) { const script = document.createElement('script'); script.src = src; if (attrs) { // eslint-disable-next-line no-restricted-syntax, guard-for-in for (const attr in attrs) { script.setAttribute(attr, attrs[attr]); } } script.onload = resolve; script.onerror = reject; document.head.append(script); } else { resolve(); } }); } /** * Retrieves the content of metadata tags. * @param {string} name The metadata name (or property) * @param {Document} doc Document object to query for metadata. Defaults to the window's document * @returns {string} The metadata value(s) */ function getMetadata(name, doc = document) { const attr = name && name.includes(':') ? 'property' : 'name'; const meta = [...doc.head.querySelectorAll(`meta[${attr}="${name}"]`)] .map((m) => m.content) .join(', '); return meta || ''; } /** * Returns a picture element with webp and fallbacks * @param {string} src The image URL * @param {string} [alt] The image alternative text * @param {boolean} [eager] Set loading attribute to eager * @param {Array} [breakpoints] Breakpoints and corresponding params (eg. width) * @returns {Element} The picture element */ function createOptimizedPicture( src, alt = '', eager = false, breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }], ) { const url = new URL(src, window.location.href); const picture = document.createElement('picture'); const { pathname } = url; const ext = pathname.substring(pathname.lastIndexOf('.') + 1); // webp breakpoints.forEach((br) => { const source = document.createElement('source'); if (br.media) source.setAttribute('media', br.media); source.setAttribute('type', 'image/webp'); source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); picture.appendChild(source); }); // fallback breakpoints.forEach((br, i) => { if (i < breakpoints.length - 1) { const source = document.createElement('source'); if (br.media) source.setAttribute('media', br.media); source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); picture.appendChild(source); } else { const img = document.createElement('img'); img.setAttribute('loading', eager ? 'eager' : 'lazy'); img.setAttribute('alt', alt); picture.appendChild(img); img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); } }); return picture; } /** * Set template (page structure) and theme (page styles). */ function decorateTemplateAndTheme() { const addClasses = (element, classes) => { classes.split(',').forEach((c) => { element.classList.add(toClassName(c.trim())); }); }; const template = getMetadata('template'); if (template) addClasses(document.body, template); const theme = getMetadata('theme'); if (theme) addClasses(document.body, theme); } /** * Wrap inline text content of block cells within a

tag. * @param {Element} block the block element */ function wrapTextNodes(block) { const validWrappers = [ 'P', 'PRE', 'UL', 'OL', 'PICTURE', 'TABLE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HR', ]; const wrap = (el) => { const wrapper = document.createElement('p'); wrapper.append(...el.childNodes); [...el.attributes] // move the instrumentation from the cell to the new paragraph, also keep the class // in case the content is a buttton and the cell the button-container .filter(({ nodeName }) => nodeName === 'class' || nodeName.startsWith('data-aue') || nodeName.startsWith('data-richtext')) .forEach(({ nodeName, nodeValue }) => { wrapper.setAttribute(nodeName, nodeValue); el.removeAttribute(nodeName); }); el.append(wrapper); }; block.querySelectorAll(':scope > div > div').forEach((blockColumn) => { if (blockColumn.hasChildNodes()) { const hasWrapper = !!blockColumn.firstElementChild && validWrappers.some((tagName) => blockColumn.firstElementChild.tagName === tagName); if (!hasWrapper) { wrap(blockColumn); } else if ( blockColumn.firstElementChild.tagName === 'PICTURE' && (blockColumn.children.length > 1 || !!blockColumn.textContent.trim()) ) { wrap(blockColumn); } } }); } /** * Decorates paragraphs containing a single link as buttons. * @param {Element} element container element */ function decorateButtons(element) { const hasInlineIconTokenSyntax = (value = '') => /\[[a-z0-9][a-z0-9-]*(?:\.svg)?\]/i.test(value); element.querySelectorAll('a').forEach((a) => { a.title = a.title || a.textContent; if (hasInlineIconTokenSyntax(a.textContent)) return; if (a.href !== a.textContent) { const up = a.parentElement; const twoup = a.parentElement.parentElement; if (!a.querySelector('img')) { if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { a.className = 'button'; // default up.classList.add('button-container'); } if ( up.childNodes.length === 1 && up.tagName === 'STRONG' && twoup.childNodes.length === 1 && twoup.tagName === 'P' ) { a.className = 'button primary'; twoup.classList.add('button-container'); } if ( up.childNodes.length === 1 && up.tagName === 'EM' && twoup.childNodes.length === 1 && twoup.tagName === 'P' ) { a.className = 'button secondary'; twoup.classList.add('button-container'); } } } }); } /** * Add for icon, prefixed with codeBasePath and optional prefix. * @param {Element} [span] span element with icon classes * @param {string} [prefix] prefix to be added to icon src * @param {string} [alt] alt text to be added to icon */ function decorateIcon(span, prefix = '', alt = '') { const iconName = Array.from(span.classList) .find((c) => c.startsWith('icon-')) .substring(5); const img = document.createElement('img'); img.dataset.iconName = iconName; img.src = `${window.hlx.codeBasePath}${prefix}/icons/${iconName}.svg`; img.alt = alt; img.loading = 'lazy'; img.width = 16; img.height = 16; span.append(img); } /** * Add for icons, prefixed with codeBasePath and optional prefix. * @param {Element} [element] Element containing icons * @param {string} [prefix] prefix to be added to icon the src */ function decorateIcons(element, prefix = '') { const icons = element.querySelectorAll('span.icon'); icons.forEach((span) => { decorateIcon(span, prefix); }); } /** * Replace inline icon tokens like [lock] or [lock.svg] with local icon images. * Tokens followed by ':' are ignored to avoid interfering with block-specific syntaxes * such as [icon-name]: Label. * @param {ParentNode} [container] Root element to process */ function decorateInlineIconTokens(container = document) { if (!container?.ownerDocument && container !== document) return; const root = container === document ? document.body : container; if (!root) return; const isSafeLinkHref = (href) => { if (!href) return false; const value = href.trim(); if (!value) return false; if (value.startsWith('/') || value.startsWith('#') || value.startsWith('./') || value.startsWith('../')) { return true; } try { const parsed = new URL(value, document.location.origin); return ['http:', 'https:', 'mailto:', 'tel:'].includes(parsed.protocol); } catch { return false; } }; const applyNewTabBehavior = (link) => { if (!link) return; link.target = '_blank'; const relValues = new Set((link.getAttribute('rel') || '').split(/\s+/).filter(Boolean)); relValues.add('noopener'); relValues.add('noreferrer'); link.setAttribute('rel', [...relValues].join(' ')); }; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { const text = node.textContent; if (!text || !text.includes('[') || !text.includes(']')) { return NodeFilter.FILTER_REJECT; } const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; if (parent.closest('script, style, noscript, textarea, pre, code')) { return NodeFilter.FILTER_REJECT; } return /\[([a-z0-9][a-z0-9-]*(?:\.svg)?)\](?!\s*:)(?:\(([^)\s]+)\))?/i.test(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, }); const textNodes = []; let current = walker.nextNode(); while (current) { textNodes.push(current); current = walker.nextNode(); } const tokenRe = /\[([a-z0-9][a-z0-9-]*(?:\.svg)?)\](?!\s*:)(?:\(([^)\s]+)\))?/gi; const basePath = `${window.hlx?.codeBasePath || ''}/icons`; textNodes.forEach((node) => { const sourceText = node.textContent || ''; tokenRe.lastIndex = 0; let match = tokenRe.exec(sourceText); if (!match) return; const fragment = document.createDocumentFragment(); let cursor = 0; while (match) { const [rawMatch, rawName, rawHref] = match; const start = match.index; if (start > cursor) { let textChunk = sourceText.slice(cursor, start); // Keep icon token visually attached to preceding word to avoid awkward wraps. if (/\S\s$/.test(textChunk)) { textChunk = `${textChunk.slice(0, -1)}\u00A0`; } fragment.append(document.createTextNode(textChunk)); } const filename = rawName.toLowerCase().endsWith('.svg') ? rawName.toLowerCase() : `${rawName.toLowerCase()}.svg`; const isExternalLinkToken = /^external.*\.svg$/.test(filename); const parentAnchor = node.parentElement?.closest('a[href]'); const img = document.createElement('img'); img.className = 'inline-icon-token'; img.dataset.iconToken = filename.replace(/\.svg$/, ''); img.src = `${basePath}/${filename}`; img.alt = ''; img.setAttribute('aria-hidden', 'true'); img.loading = 'lazy'; img.decoding = 'async'; img.width = 16; img.height = 16; if (rawHref && isSafeLinkHref(rawHref) && !node.parentElement?.closest('a[href]')) { const link = document.createElement('a'); link.href = rawHref; link.className = 'inline-icon-token-link'; if (isExternalLinkToken || /^https?:\/\//i.test(rawHref)) { applyNewTabBehavior(link); } link.append(img); fragment.append(link); } else { if (isExternalLinkToken && parentAnchor) { applyNewTabBehavior(parentAnchor); } fragment.append(img); } cursor = start + rawMatch.length; match = tokenRe.exec(sourceText); } if (cursor < sourceText.length) { fragment.append(document.createTextNode(sourceText.slice(cursor))); } node.replaceWith(fragment); }); } /** * Decorates all sections in a container element. * @param {Element} main The container element */ function decorateSections(main) { main.querySelectorAll(':scope > :is(div, section):not([data-section-status])').forEach((sectionElement) => { let section = sectionElement; if (section.tagName === 'DIV') { const semanticSection = document.createElement('section'); [...section.attributes].forEach((attr) => { semanticSection.setAttribute(attr.name, attr.value); }); while (section.firstChild) { semanticSection.append(section.firstChild); } section.replaceWith(semanticSection); section = semanticSection; } const wrappers = []; let defaultContent = false; [...section.children].forEach((e) => { if ((e.tagName === 'DIV' && e.className) || !defaultContent) { const wrapper = document.createElement('div'); wrappers.push(wrapper); defaultContent = e.tagName !== 'DIV' || !e.className; if (defaultContent) wrapper.classList.add('default-content-wrapper'); } wrappers.at(-1).append(e); }); wrappers.forEach((wrapper) => section.append(wrapper)); section.classList.add('section'); section.dataset.sectionStatus = 'initialized'; section.style.display = 'none'; // Process section metadata const sectionMeta = section.querySelector('.section-metadata'); if (sectionMeta) { const meta = readBlockConfig(sectionMeta); Object.keys(meta).forEach((key) => { if (key === 'style') { const styles = meta.style .split(',') .filter(Boolean) .map((style) => toClassName(style.trim())); styles.forEach((style) => section.classList.add(style)); } else if (key === 'anchor-id' || key === 'anchorid') { section.id = meta[key]; } else { section.dataset[toCamelCase(key)] = meta[key]; } }); sectionMeta.parentNode.remove(); } }); } /** * Builds a block DOM Element from a two dimensional array, string, or object * @param {string} blockName name of the block * @param {*} content two dimensional array or string or object of content */ function buildBlock(blockName, content) { const table = Array.isArray(content) ? content : [[content]]; const blockEl = document.createElement('div'); // build image block nested div structure blockEl.classList.add(blockName); table.forEach((row) => { const rowEl = document.createElement('div'); row.forEach((col) => { const colEl = document.createElement('div'); const vals = col.elems ? col.elems : [col]; vals.forEach((val) => { if (val) { if (typeof val === 'string') { colEl.innerHTML += val; } else { colEl.appendChild(val); } } }); rowEl.appendChild(colEl); }); blockEl.appendChild(rowEl); }); return blockEl; } /** * Loads JS and CSS for a block. * @param {Element} block The block element */ async function loadBlock(block) { const status = block.dataset.blockStatus; if (status !== 'loading' && status !== 'loaded') { block.dataset.blockStatus = 'loading'; const { blockName } = block.dataset; try { const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`); const decorationComplete = new Promise((resolve) => { (async () => { try { const mod = await import( `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js` ); if (mod.default) { await mod.default(block); } } catch (error) { // eslint-disable-next-line no-console console.error(`failed to load module for ${blockName}`, error); } resolve(); })(); }); await Promise.all([cssLoaded, decorationComplete]); } catch (error) { // eslint-disable-next-line no-console console.error(`failed to load block ${blockName}`, error); } block.dataset.blockStatus = 'loaded'; } return block; } /** * Decorates a block. * @param {Element} block The block element */ function decorateBlock(block) { const shortBlockName = block.classList[0]; if (shortBlockName && !block.dataset.blockStatus) { block.classList.add('block'); block.dataset.blockName = shortBlockName; block.dataset.blockStatus = 'initialized'; wrapTextNodes(block); const blockWrapper = block.parentElement; blockWrapper.classList.add(`${shortBlockName}-wrapper`); const section = block.closest('.section'); if (section) section.classList.add(`${shortBlockName}-container`); // eslint-disable-next-line no-use-before-define decorateButtons(block); } } /** * Decorates all blocks in a container element. * @param {Element} main The container element */ function decorateBlocks(main) { main.querySelectorAll('.section > div > div').forEach(decorateBlock); } /** * Loads a block named 'header' into header * @param {Element} header header element * @returns {Promise} */ async function loadHeader(header) { const headerBlock = buildBlock('header', ''); header.append(headerBlock); decorateBlock(headerBlock); return loadBlock(headerBlock); } /** * Loads a block named 'footer' into footer * @param footer footer element * @returns {Promise} */ async function loadFooter(footer) { const footerBlock = buildBlock('footer', ''); footer.append(footerBlock); decorateBlock(footerBlock); return loadBlock(footerBlock); } /** * Wait for Image. * @param {Element} section section element */ async function waitForFirstImage(section) { const lcpCandidate = section.querySelector('img'); await new Promise((resolve) => { if (lcpCandidate && !lcpCandidate.complete) { lcpCandidate.setAttribute('loading', 'eager'); lcpCandidate.addEventListener('load', resolve); lcpCandidate.addEventListener('error', resolve); } else { resolve(); } }); } /** * Loads all blocks in a section. * @param {Element} section The section element */ async function loadSection(section, loadCallback) { const status = section.dataset.sectionStatus; if (!status || status === 'initialized') { section.dataset.sectionStatus = 'loading'; const blocks = [...section.querySelectorAll('div.block')]; for (let i = 0; i < blocks.length; i += 1) { // eslint-disable-next-line no-await-in-loop await loadBlock(blocks[i]); } if (loadCallback) await loadCallback(section); section.dataset.sectionStatus = 'loaded'; section.style.display = null; } } /** * Loads all sections. * @param {Element} element The parent element of sections to load */ async function loadSections(element) { const sections = [...element.querySelectorAll('.section')]; for (let i = 0; i < sections.length; i += 1) { // eslint-disable-next-line no-await-in-loop await loadSection(sections[i]); if (i === 0 && sampleRUM.enhance) { sampleRUM.enhance(); } } } init(); export { buildBlock, createOptimizedPicture, decorateBlock, decorateBlocks, decorateButtons, decorateInlineIconTokens, decorateIcons, decorateSections, decorateTemplateAndTheme, getMetadata, loadBlock, loadCSS, loadFooter, loadHeader, loadScript, loadSection, loadSections, readBlockConfig, sampleRUM, setup, toCamelCase, toClassName, waitForFirstImage, wrapTextNodes, };