/* * 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,
};