1/* 2 * Copyright 2024 Adobe. All rights reserved. 3 * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. You may obtain a copy 5 * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 * 7 * Unless required by applicable law or agreed to in writing, software distributed under 8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 * OF ANY KIND, either express or implied. See the License for the specific language 10 * governing permissions and limitations under the License. 11 */ 12 13/* eslint-env browser */ 14function sampleRUM(checkpoint, data) { 15 // eslint-disable-next-line max-len 16 const timeShift = () => (window.performance ? window.performance.now() : Date.now() - window.hlx.rum.firstReadTime); 17 try { 18 window.hlx = window.hlx || {}; 19 if (!window.hlx.rum || !window.hlx.rum.collector) { 20 sampleRUM.enhance = () => { }; 21 const params = new URLSearchParams(window.location.search); 22 const { currentScript } = document; 23 const rate = params.get('rum') 24 || window.SAMPLE_PAGEVIEWS_AT_RATE 25 || params.get('optel') 26 || (currentScript && currentScript.dataset.rate); 27 const rateValue = { 28 on: 1, 29 off: 0, 30 high: 10, 31 low: 1000, 32 }[rate]; 33 const weight = rateValue !== undefined ? rateValue : 100; 34 const id = (window.hlx.rum && window.hlx.rum.id) || crypto.randomUUID().slice(-9); 35 const isSelected = (window.hlx.rum && window.hlx.rum.isSelected) 36 || (weight > 0 && Math.random() * weight < 1); 37 // eslint-disable-next-line object-curly-newline, max-len 38 window.hlx.rum = { 39 weight, 40 id, 41 isSelected, 42 firstReadTime: window.performance ? window.performance.timeOrigin : Date.now(), 43 sampleRUM, 44 queue: [], 45 collector: (...args) => window.hlx.rum.queue.push(args), 46 }; 47 if (isSelected) { 48 const dataFromErrorObj = (error) => { 49 const errData = { source: 'undefined error' }; 50 try { 51 errData.target = error.toString(); 52 if (error.stack) { 53 errData.source = error.stack 54 .split('\n') 55 .filter((line) => line.match(/https?:\/\//)) 56 .shift() 57 .replace(/at ([^ ]+) \((.+)\)/, '$1@$2') 58 .replace(/ at /, '@') 59 .trim(); 60 } 61 } catch (err) { 62 /* error structure was not as expected */ 63 } 64 return errData; 65 }; 66 67 window.addEventListener('error', ({ error }) => { 68 const errData = dataFromErrorObj(error); 69 sampleRUM('error', errData); 70 }); 71 72 window.addEventListener('unhandledrejection', ({ reason }) => { 73 let errData = { 74 source: 'Unhandled Rejection', 75 target: reason || 'Unknown', 76 }; 77 if (reason instanceof Error) { 78 errData = dataFromErrorObj(reason); 79 } 80 sampleRUM('error', errData); 81 }); 82 83 window.addEventListener('securitypolicyviolation', (e) => { 84 if (e.blockedURI.includes('helix-rum-enhancer') && e.disposition === 'enforce') { 85 const errData = { 86 source: 'csp', 87 target: e.blockedURI, 88 }; 89 sampleRUM.sendPing('error', timeShift(), errData); 90 } 91 }); 92 93 sampleRUM.baseURL = sampleRUM.baseURL || new URL(window.RUM_BASE || '/', new URL('https://ot.aem.live')); 94 sampleRUM.collectBaseURL = sampleRUM.collectBaseURL || sampleRUM.baseURL; 95 sampleRUM.sendPing = (ck, time, pingData = {}) => { 96 // eslint-disable-next-line max-len, object-curly-newline 97 const rumData = JSON.stringify({ 98 weight, 99 id, 100 referer: window.location.href, 101 checkpoint: ck, 102 t: time, 103 ...pingData, 104 }); 105 const urlParams = window.RUM_PARAMS 106 ? new URLSearchParams(window.RUM_PARAMS).toString() || '' 107 : ''; 108 const { href: url, origin } = new URL( 109 `.rum/${weight}
109${urlParams ? `?${urlParams}` : ''}`, 110 sampleRUM.collectBaseURL, 111 ); 112 const body = origin === window.location.origin 113 ? new Blob([rumData], { type: 'application/json' }) 114 : rumData; 115 navigator.sendBeacon(url, body); 116 // eslint-disable-next-line no-console 117 console.debug(`ping:${ck}`, pingData); 118 }; 119 sampleRUM.sendPing('top', timeShift()); 120 121 sampleRUM.enhance = () => { 122 // only enhance once 123 if (document.querySelector('script[src*="rum-enhancer"]')) return; 124 const { enhancerVersion, enhancerHash } = sampleRUM.enhancerContext || {}; 125 const script = document.createElement('script'); 126 if (enhancerHash) { 127 script.integrity = enhancerHash; 128 script.setAttribute('crossorigin', 'anonymous'); 129 } 130 script.src = new URL( 131 `.rum/@adobe/helix-rum-enhancer@${enhancerVersion || '^2'}/src/index.js`, 132 sampleRUM.baseURL, 133 ).href; 134 document.head.appendChild(script); 135 }; 136 if (!window.hlx.RUM_MANUAL_ENHANCE) { 137 sampleRUM.enhance(); 138 } 139 } 140 } 141 if (window.hlx.rum && window.hlx.rum.isSelected && checkpoint) { 142 window.hlx.rum.collector(checkpoint, data, timeShift()); 143 } 144 document.dispatchEvent(new CustomEvent('rum', { detail: { checkpoint, data } })); 145 } catch (error) { 146 // something went awry 147 } 148} 149 150/** 151 * Setup block utils. 152 */ 153function setup() { 154 window.hlx = window.hlx || {}; 155 window.hlx.RUM_MASK_URL = 'full'; 156 window.hlx.RUM_MANUAL_ENHANCE = true; 157 window.hlx.codeBasePath = ''; 158 window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; 159 160 const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); 161 if (scriptEl) { 162 try { 163 [window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js'); 164 } catch (error) { 165 // eslint-disable-next-line no-console 166 console.log(error); 167 } 168 } 169} 170 171/** 172 * Auto initialization. 173 */ 174 175function init() { 176 setup(); 177 sampleRUM.collectBaseURL = window.origin; 178 sampleRUM(); 179} 180 181/** 182 * Sanitizes a string for use as class name. 183 * @param {string} name The unsanitized string 184 * @returns {string} The class name 185 */ 186function toClassName(name) { 187 return typeof name === 'string' 188 ? name 189 .toLowerCase() 190 .replace(/[^0-9a-z]/gi, '-') 191 .replace(/-+/g, '-') 192 .replace(/^-|-$/g, '') 193 : ''; 194} 195 196/** 197 * Sanitizes a string for use as a js property name. 198 * @param {string} name The unsanitized string 199 * @returns {string} The camelCased name 200 */ 201function toCamelCase(name) { 202 return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 203} 204 205/** 206 * Extracts the config from a block. 207 * @param {Element} block The block element 208 * @returns {object} The block config 209 */ 210// eslint-disable-next-line import/prefer-default-export 211function readBlockConfig(block) { 212 const config = {}; 213 [...block.children].forEach((row) => { 214 if (row.children) { 215 const cols = [...row.children]; 216 if (cols[1]) { 217 const col = cols[1]; 218 const name = toClassName(cols[0].textContent); 219 let value = ''; 220 if (col.querySelector('a')) { 221 const as = [...col.querySelectorAll('a')]; 222 if (as.length === 1) { 223 value = as[0].href; 224 } else { 225 value = as.map((a) => a.href); 226 } 227 } else if (col.querySelector('img')) { 228 const imgs = [...col.querySelectorAll('img')]; 229 if (imgs.length === 1) { 230 value = imgs[0].src; 231 } else { 232 value = imgs.map((img) => img.src); 233 } 234 } else if (col.querySelector('p')) { 235 const ps = [...col.querySelectorAll('p')]; 236 if (ps.length === 1) { 237 value = ps[0].textContent; 238 } else { 239 value = ps.map((p) => p.textContent); 240 } 241 } else value = row.children[1].textContent; 242 config[name] = value; 243 } 244 } 245 }); 246 return config; 247} 248
249/** 250 * Loads a CSS file. 251 * @param {string} href URL to the CSS file 252 */ 253async function loadCSS(href) { 254 return new Promise((resolve, reject) => { 255 if (!document.querySelector(`head > link[href="${href}"]`)) { 256 const link = document.createElement('link'); 257 link.rel = 'stylesheet'; 258 link.href = href; 259 link.onload = resolve; 260 link.onerror = reject; 261 document.head.append(link); 262 } else { 263 resolve(); 264 } 265 }); 266} 267 268/** 269 * Loads a non module JS file. 270 * @param {string} src URL to the JS file 271 * @param {Object} attrs additional optional attributes 272 */ 273async function loadScript(src, attrs) { 274 return new Promise((resolve, reject) => { 275 if (!document.querySelector(`head > script[src="${src}"]`)) { 276 const script = document.createElement('script'); 277 script.src = src; 278 if (attrs) { 279 // eslint-disable-next-line no-restricted-syntax, guard-for-in 280 for (const attr in attrs) { 281 script.setAttribute(attr, attrs[attr]); 282 } 283 } 284 script.onload = resolve; 285 script.onerror = reject; 286 document.head.append(script); 287 } else { 288 resolve(); 289 } 290 }); 291} 292 293/** 294 * Retrieves the content of metadata tags. 295 * @param {string} name The metadata name (or property) 296 * @param {Document} doc Document object to query for metadata. Defaults to the window's document 297 * @returns {string} The metadata value(s) 298 */ 299function getMetadata(name, doc = document) { 300 const attr = name && name.includes(':') ? 'property' : 'name'; 301 const meta = [...doc.head.querySelectorAll(`meta[${attr}="${name}"]`)] 302 .map((m) => m.content) 303 .join(', '); 304 return meta || ''; 305} 306 307/** 308 * Returns a picture element with webp and fallbacks 309 * @param {string} src The image URL 310 * @param {string} [alt] The image alternative text 311 * @param {boolean} [eager] Set loading attribute to eager 312 * @param {Array} [breakpoints] Breakpoints and corresponding params (eg. width) 313 * @returns {Element} The picture element 314 */ 315function createOptimizedPicture( 316 src, 317 alt = '', 318 eager = false, 319 breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }], 320) { 321 const url = !src.startsWith('http') ? new URL(src, window.location.href) : new URL(src); 322 const picture = document.createElement('picture'); 323 const { origin, pathname } = url; 324 const ext = pathname.split('.').pop(); 325 326 // webp 327 breakpoints.forEach((br) => { 328 const source = document.createElement('source'); 329 if (br.media) source.setAttribute('media', br.media); 330 source.setAttribute('type', 'image/webp'); 331 source.setAttribute('srcset', `${origin}${pathname}?width=${br.width}&format=webply&optimize=medium`); 332 picture.appendChild(source); 333 }); 334 335 // fallback 336 breakpoints.forEach((br, i) => { 337 if (i < breakpoints.length - 1) { 338 const source = document.createElement('source'); 339 if (br.media) source.setAttribute('media', br.media); 340 source.setAttribute('srcset', `${origin}${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 341 picture.appendChild(source); 342 } else { 343 const img = document.createElement('img'); 344 img.setAttribute('loading', eager ? 'eager' : 'lazy'); 345 img.setAttribute('alt', alt); 346 picture.appendChild(img); 347 img.setAttribute('src', `${origin}${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 348 } 349 }); 350 351 return picture; 352} 353 354/** 355 * Set template (page structure) and theme (page styles). 356 */ 357function decorateTemplateAndTheme() { 358 const addClasses = (element, classes) => { 359 classes.split(',').forEach((c) => { 360 element.classList.add(toClassName(c.trim())); 361 }); 362 }; 363 const template = getMetadata('template'); 364 if (template) addClasses(document.body, template); 365 const theme = getMetadata('theme'); 366 if (theme) addClasses(document.body, theme); 367} 368 369/** 370 * Wrap inline text content of block cells within a <p> tag. 371 * @param {Element} block the block element 372 */ 373function wrapTextNodes(block) { 374 const validWrappers = [ 375 'P', 376 'PRE', 377 'UL', 378 'OL', 379 'PICTURE', 380 'TABLE', 381 'H1', 382 'H2', 383 'H3', 384 'H4', 385 'H5', 386 'H6', 387 ]; 388 389 const wrap = (el) => { 390 const wrapper = document.createElement('p'); 391 wrapper.append(...el.childNodes); 392 el.append(wrapper); 393 }; 394 395 block.querySelectorAll(':scope > div > div').forEach((blockColumn) => { 396 if (blockColumn.hasChildNodes()) { 397 const hasWrapper = !!blockColumn.firstElementChild 398 && validWrappers.some((tagName) => blockColumn.firstElementChild.tagName === tagName); 399 if (!hasWrapper) { 400 wrap(blockColumn); 401 } else if ( 402 blockColumn.firstElementChild.tagName === 'PICTURE' 403 && (blockColumn.children.length > 1 || !!blockColumn.textContent.trim()) 404 ) { 405 wrap(blockColumn); 406 } 407 } 408 }); 409} 410 411// DEFAULT decorateButtons implementation. 412/** 413 * Decorates paragraphs containing a single link as buttons. 414 * @param {Element} element container element 415 */ 416function decorateButtons(element) { 417 element.querySelectorAll('a').forEach((a) => { 418 a.title = a.title || a.textContent; 419 if (a.href !== a.textContent) { 420 const up = a.parentElement; 421 const twoup = a.parentElement.parentElement; 422 if (!a.querySelector('img')) { 423 if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { 424 a.className = 'button'; // default 425 up.classList.add('button-container'); 426 } 427 if ( 428 up.childNodes.length === 1 429 && up.tagName === 'STRONG' 430 && twoup.childNodes.length === 1 431 && twoup.tagName === 'P' 432 ) { 433 a.className = 'button primary'; 434 twoup.classList.add('button-container'); 435 } 436 if ( 437 up.childNodes.length === 1 438 && up.tagName === 'EM' 439 && twoup.childNodes.length === 1 440 && twoup.tagName === 'P' 441 ) { 442 a.className = 'button secondary'; 443 twoup.classList.add('button-container'); 444 } 445 } 446 } 447 }); 448} 449 450/** 451 * Add <img> for icon, prefixed with codeBasePath and optional prefix. 452 * @param {Element} [span] span element with icon classes 453 * @param {string} [prefix] prefix to be added to icon src 454 * @param {string} [alt] alt text to be added to icon 455 */ 456function decorateIcon(span, prefix = '', alt = '') { 457 const iconName = Array.from(span.classList) 458 .find((c) => c.startsWith('icon-')) 459 .substring(5); 460 const img = document.createElement('img'); 461 img.dataset.iconName = iconName; 462 img.src = `${window.hlx.codeBasePath}${prefix}/icons/${iconName}.svg`; 463 img.alt = alt; 464 img.loading = 'lazy'; 465 span.append(img); 466} 467 468/** 469 * Add <img> for icons, prefixed with codeBasePath and optional prefix. 470 * @param {Element} [element] Element containing icons 471 * @param {string} [prefix] prefix to be added to icon the src 472 */ 473function decorateIcons(element, prefix = '') { 474 const icons = [...element.querySelectorAll('span.icon')]; 475 icons.forEach((span) => { 476 decorateIcon(span, prefix); 477 }); 478} 479 480/** 481 * Decorates all sections in a container element. 482 * @param {Element} main The container element 483 */ 484function decorateSections(main) { 485 main.querySelectorAll(':scope > div').forEach((section) => { 486 const wrappers = []; 487 let defaultContent = false;
488 [...section.children].forEach((e) => { 489 if (e.tagName === 'DIV' || !defaultContent) { 490 const wrapper = document.createElement('div'); 491 wrappers.push(wrapper); 492 defaultContent = e.tagName !== 'DIV'; 493 if (defaultContent) wrapper.classList.add('default-content-wrapper'); 494 } 495 wrappers[wrappers.length - 1].append(e); 496 }); 497 wrappers.forEach((wrapper) => section.append(wrapper)); 498 section.classList.add('section'); 499 section.dataset.sectionStatus = 'initialized'; 500 section.style.display = 'none'; 501 502 // Process section metadata 503 const sectionMeta = section.querySelector('div.section-metadata'); 504 if (sectionMeta) { 505 const meta = readBlockConfig(sectionMeta); 506 Object.keys(meta).forEach((key) => { 507 if (key === 'style') { 508 const styles = meta.style 509 .split(',') 510 .filter((style) => style) 511 .map((style) => toClassName(style.trim())); 512 styles.forEach((style) => section.classList.add(style)); 513 } else { 514 section.dataset[toCamelCase(key)] = meta[key]; 515 } 516 }); 517 sectionMeta.parentNode.remove(); 518 } 519 }); 520} 521 522/** 523 * Gets placeholders object. 524 * @param {string} [prefix] Location of placeholders 525 * @returns {object} Window placeholders object 526 */ 527// eslint-disable-next-line import/prefer-default-export 528async function fetchPlaceholders(prefix = 'default') { 529 window.placeholders = window.placeholders || {}; 530 if (!window.placeholders[prefix]) { 531 window.placeholders[prefix] = new Promise((resolve) => { 532 fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`) 533 .then((resp) => { 534 if (resp.ok) { 535 return resp.json(); 536 } 537 return {}; 538 }) 539 .then((json) => { 540 const placeholders = {}; 541 json.data 542 .filter((placeholder) => placeholder.Key) 543 .forEach((placeholder) => { 544 placeholders[toCamelCase(placeholder.Key)] = placeholder.Text; 545 }); 546 window.placeholders[prefix] = placeholders; 547 resolve(window.placeholders[prefix]); 548 }) 549 .catch(() => { 550 // error loading placeholders 551 window.placeholders[prefix] = {}; 552 resolve(window.placeholders[prefix]); 553 }); 554 }); 555 } 556 return window.placeholders[`${prefix}`]; 557} 558 559/** 560 * Builds a block DOM Element from a two dimensional array, string, or object 561 * @param {string} blockName name of the block 562 * @param {*} content two dimensional array or string or object of content 563 */ 564function buildBlock(blockName, content) { 565 const table = Array.isArray(content) ? content : [[content]]; 566 const blockEl = document.createElement('div'); 567 // build image block nested div structure 568 blockEl.classList.add(blockName); 569 table.forEach((row) => { 570 const rowEl = document.createElement('div'); 571 row.forEach((col) => { 572 const colEl = document.createElement('div'); 573 const vals = col.elems ? col.elems : [col]; 574 vals.forEach((val) => { 575 if (val) { 576 if (typeof val === 'string') { 577 colEl.innerHTML += val; 578 } else { 579 colEl.appendChild(val); 580 } 581 } 582 }); 583 rowEl.appendChild(colEl); 584 }); 585 blockEl.appendChild(rowEl); 586 }); 587 return blockEl; 588} 589 590/** 591 * Loads JS and CSS for a block. 592 * @param {Element} block The block element 593 */ 594async function loadBlock(block) { 595 const status = block.dataset.blockStatus; 596 const isSSR = block.dataset.ssr === 'true'; 597 if (status !== 'loading' && (status !== 'loaded' || isSSR)) { 598 if (!isSSR) block.dataset.blockStatus = 'loading'; 599 const { blockName } = block.dataset; 600 try { 601 const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`); 602 const decorationComplete = new Promise((resolve) => { 603 (async () => { 604 try { 605 const mod = await import( 606 `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js` 607 ); 608 if (mod.default) { 609 await mod.default(block); 610 } 611 } catch (error) { 612 // eslint-disable-next-line no-console 613 console.log(`failed to load module for ${blockName}`, error); 614 } 615 resolve(); 616 })(); 617 }); 618 await Promise.all([cssLoaded, decorationComplete]); 619 } catch (error) { 620 // eslint-disable-next-line no-console 621 console.log(`failed to load block ${blockName}`, error); 622 } 623 block.dataset.blockStatus = 'loaded'; 624 } 625 return block; 626} 627 628/** 629 * Decorates a block. 630 * @param {Element} block The block element 631 */ 632function decorateBlock(block) { 633 const shortBlockName = block.classList[0]; 634 if (shortBlockName) { 635 block.classList.add('block'); 636 block.dataset.blockName = shortBlockName; 637 // Preserve SSR block status â skip re-initialization and wrapTextNodes 638 if (block.dataset.ssr === 'true' && block.dataset.blockStatus === 'loaded') { 639 // SSR'd block: keep status as 'loaded', skip wrapTextNodes to preserve rendered HTML 640 } else { 641 block.dataset.blockStatus = 'initialized'; 642 wrapTextNodes(block); 643 } 644 const blockWrapper = block.parentElement; 645 blockWrapper.classList.add(`${shortBlockName}-wrapper`); 646 const section = block.closest('.section'); 647 if (section) section.classList.add(`${shortBlockName}-container`); 648 } 649} 650 651/** 652 * Decorates all blocks in a container element. 653 * @param {Element} main The container element 654 */ 655function decorateBlocks(main) { 656 main.querySelectorAll('div.section > div > div').forEach(decorateBlock); 657} 658 659/** 660 * Loads a block named 'header' into header 661 * @param {Element} header header element 662 * @returns {Promise} 663 */ 664async function loadHeader(header) { 665 const headerBlock = buildBlock('header', ''); 666 header.append(headerBlock); 667 decorateBlock(headerBlock); 668 return loadBlock(headerBlock); 669} 670 671/** 672 * Loads a block named 'footer' into footer 673 * @param footer footer element 674 * @returns {Promise} 675 */ 676async function loadFooter(footer) { 677 const footerBlock = buildBlock('footer', ''); 678 footer.append(footerBlock); 679 decorateBlock(footerBlock); 680 return loadBlock(footerBlock); 681} 682 683/** 684 * Wait for Image. 685 * @param {Element} section section element 686 */ 687async function waitForFirstImage(section) { 688 const lcpCandidate = section.querySelector('img'); 689 await new Promise((resolve) => { 690 if (lcpCandidate && !lcpCandidate.complete) { 691 lcpCandidate.setAttribute('loading', 'eager'); 692 lcpCandidate.addEventListener('load', resolve); 693 lcpCandidate.addEventListener('error', resolve); 694 } else { 695 resolve(); 696 } 697 }); 698} 699 700/** 701 * Loads all blocks in a section. 702 * @param {Element} section The section element 703 */ 704 705async function loadSection(section, loadCallback) { 706 const status = section.dataset.sectionStatus; 707 if (!status || status === 'initialized') { 708 section.dataset.sectionStatus = 'loading'; 709 const blocks = [...section.querySelectorAll('div.block')]; 710 for (let i = 0; i < blocks.length; i += 1) { 711 // eslint-disable-next-line no-await-in-loop 712 await loadBlock(blocks[i]); 713 } 714 if (loadCallback) await loadCallback(section); 715 section.dataset.sectionStatus = 'loaded'; 716 section.style.display = null; 717 } 718} 719 720/** 721 * Loads all sections. 722 * @param {Element} element The parent element of sections to load 723 */ 724 725async function loadSections(element) { 726 const sections = [...element.querySelectorAll('div.section')]; 727 for (let i = 0; i < sections.length; i += 1) { 728 // eslint-disable-next-line no-await-in-loop 729 await loadSection(sections[i]); 730 if (i === 0 && sampleRUM.enhance) { 731 sampleRUM.enhance(); 732 } 733 } 734} 735 736init(); 737 738export { 739 buildBlock, 740 createOptimizedPicture, 741 decorateBlock, 742 decorateBlocks, 743 decorateButtons, 744 decorateIcons, 745 decorateSections, 746 decorateTemplateAndTheme, 747 fetchPlaceholders, 748 getMetadata, 749 init, 750 loadBlock, 751 loadCSS, 752 loadFooter, 753 loadHeader, 754 loadScript, 755 loadSection, 756 loadSections, 757 readBlockConfig, 758 sampleRUM, 759 setup, 760 toCamelCase, 761 toClassName, 762 waitForFirstImage, 763 wrapTextNodes, 764};
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.