PageSourceSearch

https://www.subway.com/scripts/aem.js

js subway.com collected 2026-09-24 08:30:22 UTC 23,127 bytes, 746 lines download raw bytes

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

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.