1import { 2 ANALYTICS_EVENTS, 3 ANALYTICS_WEB_INTERACTION_TYPES, 4 ANALYTICS_WEB_INTERACTION_HERO_BOOKING, 5 ANALYTICS_WEB_INTERACTION_FORM_SUBMIT, 6 CUSTOM_WEB_INTERACTION_TYPES, 7} from './constants.js'; 8import { isExternalHttpUrl } from './link-utils.js'; 9 10/** File extensions treated as document downloads (not in-page navigation). */ 11const DOWNLOAD_FILE_EXTENSIONS = new Set([ 12 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'csv', 'txt', 'rtf', 'odt', 'ods', 'odp', 13]); 14 15/** 16 * Classify a URL for `web.webInteraction.type`: in-site link, exit (external), or download. 17 * @param {string} href - Absolute or relative URL 18 * @returns {typeof ANALYTICS_WEB_INTERACTION_TYPES[keyof typeof ANALYTICS_WEB_INTERACTION_TYPES]} 19 */ 20export function getWebInteractionTypeForHref(href) { 21 if (!href || typeof href !== 'string') return ANALYTICS_WEB_INTERACTION_TYPES.LINK; 22 let parsed; 23 try { 24 const base = typeof window !== 'undefined' && window.location?.href 25 ? window.location.href 26 : ''; 27 parsed = new URL(href, base); 28 } catch { 29 return ANALYTICS_WEB_INTERACTION_TYPES.LINK; 30 } 31 const pathname = parsed.pathname.toLowerCase(); 32 if (pathname.includes('/content/dam/')) { 33 return ANALYTICS_WEB_INTERACTION_TYPES.DOWNLOAD; 34 } 35 const lastSegment = pathname.split('/').pop() || ''; 36 const dot = lastSegment.lastIndexOf('.'); 37 const ext = dot > -1 ? lastSegment.slice(dot + 1).toLowerCase() : ''; 38 if (ext && DOWNLOAD_FILE_EXTENSIONS.has(ext)) { 39 return ANALYTICS_WEB_INTERACTION_TYPES.DOWNLOAD; 40 } 41 if (isExternalHttpUrl(parsed)) { 42 return ANALYTICS_WEB_INTERACTION_TYPES.EXIT; 43 } 44 return ANALYTICS_WEB_INTERACTION_TYPES.LINK; 45} 46 47/** 48 * @param {HTMLAnchorElement} anchor 49 * @returns {boolean} 50 */ 51function isCtaAnchor(anchor) { 52 if (anchor.tagName !== 'A') return false; 53 if (anchor.matches('.button, [class*="button"]')) return true; 54 return Boolean(anchor.closest('.button-container, .cta')); 55} 56 57/** 58 * Classify click intent for `_aramarksports.web.webInteraction.type` (link vs CTA vs form). 59 * @param {Element} [element] - Click target (anchor, submit control, or click origin) 60 * @returns {typeof CUSTOM_WEB_INTERACTION_TYPES[keyof typeof CUSTOM_WEB_INTERACTION_TYPES]} 61 */ 62export function getCustomWebInteractionTypeForElement(element) { 63 if (!element || !(element instanceof Element)) { 64 return CUSTOM_WEB_INTERACTION_TYPES.LINK; 65 } 66 67 if (element.matches('button[type="submit"], input[type="submit"], input[type="image"]')) { 68 return CUSTOM_WEB_INTERACTION_TYPES.FORM; 69 } 70 71 if (element.matches('.booking-trigger')) { 72 return CUSTOM_WEB_INTERACTION_TYPES.CLICK; 73 } 74 75 const anchor = element.tagName === 'A' ? element : element.closest('a'); 76 if (anchor && isCtaAnchor(anchor)) { 77 return CUSTOM_WEB_INTERACTION_TYPES.CLICK; 78 } 79 if (anchor) { 80 return CUSTOM_WEB_INTERACTION_TYPES.LINK; 81 } 82 83 return CUSTOM_WEB_INTERACTION_TYPES.LINK; 84} 85 86/** 87 * Formats AEM product category paths for analytics display. 88 * e.g. `aramark-destinations:global/product-category/houseboat-xl` â `Houseboat Xl` 89 * @param {string} productCategory 90 * @returns {string} 91 */ 92export function formatProductCategoryName(productCategory) { 93 if (!productCategory || typeof productCategory !== 'string') return ''; 94 const trimmed = productCategory.trim(); 95 if (!trimmed) return ''; 96 const slug = trimmed.split('/').pop() || ''; 97 if (!slug) return ''; 98 return slug 99 .replace(/-/g, ' ') 100 .replace(/\b\w/g, (char) => char.toUpperCase()); 101} 102 103/** 104 * Shared Analytics Utilities 105 * Consent-gated helpers for pushing events to the Adobe Data Layer (ACDL). 106 * Import these instead of calling window.adobeDataLayer directly in blocks. 107 */ 108 109/** 110 * Client Hints (`navigator.userAgentData`) are Chromium-only. Read via Reflect so 111 * eslint-plugin-compat does not treat bare property access as unconditional use, 112 * and fall back to `navigator.userAgent` elsewhere (Safari, Firefox). 113 * 114 * @returns {object|undefined} 115 */ 116function getNavigatorUaData() { 117 if (typeof navigator === 'undefined') return undefined; 118 return Reflect.get(navigator, 'userAgentData'); 119} 120 121function inferDeviceTypeFromUserAgent(ua) { 122 if (/tablet|ipad|playbook|silk/i.test(ua)) return 'tablet'; 123 if (/mobile|iphone|ipod|android.*mobile|blackberry|opera mini|iemobile|wpdesktop/i.test(ua)) { 124 return 'mobile'; 125 } 126 return 'desktop'; 127} 128
129/** 130 * Get the device type from the user agent data. 131 * @param {object} uaData - The user agent data. 132 * @param {string} ua - The user agent. 133 * @returns {string} The device type. 134 */ 135function getAnalyticsDeviceType(uaData, ua) { 136 if (uaData && typeof uaData.mobile === 'boolean') { 137 return uaData.mobile ? 'mobile' : 'desktop'; 138 } 139 return inferDeviceTypeFromUserAgent(ua); 140} 141 142/** 143 * Get the platform name from the user agent data. 144 * @param {object} uaData - The user agent data. 145 * @returns {string} The platform name. 146 */ 147function getAnalyticsPlatformName(uaData) { 148 const fromHints = uaData && typeof uaData.platform === 'string' ? uaData.platform.trim() : ''; 149 if (fromHints) return fromHints; 150 if (typeof navigator !== 'undefined' && typeof navigator.platform === 'string') { 151 return navigator.platform; 152 } 153 return undefined; 154} 155 156/** 157 * Infer the browser name from the user agent. 158 * @param {string} ua - The user agent. 159 * @returns {string} The browser name. 160 */ 161function inferBrowserNameFromUserAgent(ua) { 162 if (/edg\//i.test(ua)) return 'Microsoft Edge'; 163 if (/opr\/|opera/i.test(ua)) return 'Opera'; 164 if (/chrome|crios/i.test(ua)) return 'Chrome'; 165 if (/firefox|fxios/i.test(ua)) return 'Firefox'; 166 if (/safari/i.test(ua) && !/chrome|crios/i.test(ua)) return 'Safari'; 167 return undefined; 168} 169 170/** 171 * Get the browser name from the user agent data. 172 * @param {object} uaData - The user agent data. 173 * @param {string} ua - The user agent. 174 * @returns {string} The browser name. 175 */ 176function getAnalyticsBrowserName(uaData, ua) { 177 const brands = uaData && uaData.brands; 178 if (Array.isArray(brands) && brands.length > 0) { 179 const meaningful = brands.find((b) => b && typeof b.brand === 'string' && !/^Not[./]/i.test(b.brand)); 180 const pick = meaningful ?? brands[0]; 181 if (pick && typeof pick.brand === 'string') return pick.brand; 182 } 183 return inferBrowserNameFromUserAgent(ua); 184} 185 186/** 187 * Check if analytics consent has been granted. 188 * @returns {boolean} True if consent granted. 189 */ 190export function checkAnalyticsConsent() { 191 // OneTrust 192 if (window.OnetrustActiveGroups) { 193 return window.OnetrustActiveGroups.includes('C0002'); // Performance cookies 194 } 195 196 // Adobe Privacy JavaScript 197 if (window.adobe?.optIn) { 198 return window.adobe.optIn.isApproved(window.adobe.OptInCategories.ANALYTICS); 199 } 200 201 // TrustArc 202 if (window.truste?.eu?.bindMap) { 203 return window.truste.eu.bindMap.performance === '1'; 204 } 205 206 // No consent framework detected â default to allowed (opt-out model) 207 return true; 208} 209 210/** sessionStorage key for the last product page the user viewed. */ 211const LAST_VIEWED_PRODUCT_NAME_KEY = 'lastViewedProductName'; 212 213let productPageLeaveTrackingAttached = false; 214 215/** 216 * Reads the previously viewed product name from sessionStorage. 217 * @returns {string} 218 */ 219export function getLastViewedProductName() { 220 if (typeof sessionStorage === 'undefined') return ''; 221 try { 222 return sessionStorage.getItem(LAST_VIEWED_PRODUCT_NAME_KEY)?.trim() || ''; 223 } catch { 224 return ''; 225 } 226} 227 228/** 229 * Persists the last viewed product name when analytics consent is granted. 230 * @param {string} name 231 */ 232function setLastViewedProductName(name) { 233 if (!checkAnalyticsConsent() || typeof sessionStorage === 'undefined') return; 234 try { 235 sessionStorage.setItem(LAST_VIEWED_PRODUCT_NAME_KEY, name); 236 } catch { 237 // Private mode / quota â ignore 238 } 239} 240 241/** 242 * Push an event to the Adobe Data Layer if ACDL is present and consent is granted. 243 * @param {string} eventName - Event name in snake_case (e.g. 'card_click') 244 * @param {Object} eventInfo - Event-specific metadata 245 * @param {Object} contextData - pageContext data to merge 246 */ 247export function pushAnalyticsEvent(eventName, eventInfo, _contextData) { 248 if (!checkAnalyticsConsent()) return; 249 250 // Queue before Launch/ACDL finishes loading â same pattern as blocks (navigation, breadcrumbs). 251 window.adobeDataLayer = window.adobeDataLayer || []; 252 253 const uaData = getNavigatorUaData(); 254 const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''; 255 256 switch (eventName) { 257 case ANALYTICS_EVENTS.PAGE_VIEW.name: 258 window.adobeDataLayer.push({ 259 event: eventName, 260 eventType: ANALYTICS_EVENTS.PAGE_VIEW.type, 261 web: { 262 webPageDetails: { 263 URL: window.location.href, 264 name: document.title, 265 }, 266 }, 267 device: { 268 type: getAnalyticsDeviceType(uaData, ua), 269 }, 270 environment: { 271 operatingSystem: getAnalyticsPlatformName(uaData), 272 browserDetails: { 273 name: getAnalyticsBrowserName(uaData, ua), 274 }, 275 }, 276 }); 277 break; 278 case ANALYTICS_EVENTS.LINK_CLICK.name: { 279 const url = eventInfo && typeof eventInfo.url === 'string' ? eventInfo.url : ''; 280 if (!url) break; 281 const interactionName = eventInfo && typeof eventInfo.name === 'string' ? eventInfo.name : ''; 282 const interactionType = getWebInteractionTypeForHref(url); 283 const customType = eventInfo && typeof eventInfo.customWebInteractionType === 'string' 284 ? eventInfo.customWebInteractionType 285 : getCustomWebInteractionTypeForElement(eventInfo?.element); 286 window.adobeDataLayer.push({ 287 event: eventName, 288 eventType: ANALYTICS_EVENTS.LINK_CLICK.type, 289 web: { 290 webInteraction: { 291 name: interactionName, 292 type: interactionType, 293 URL: url, 294 }, 295 }, 296 _aramarksports: { 297 web: { 298 webInteraction: { 299 type: customType, 300 }, 301 }, 302 }, 303 }); 304 break; 305 } 306 case ANALYTICS_EVENTS.HERO_BOOKING_SUBMIT.name: { 307 const url = eventInfo && typeof eventInfo.url === 'string' ? eventInfo.url : ''; 308 if (!url) break; 309 window.adobeDataLayer.push({ 310 event: ANALYTICS_EVENTS.LINK_CLICK.name, 311 eventType: ANALYTICS_EVENTS.HERO_BOOKING_SUBMIT.type, 312 web: { 313 webInteraction: { 314 name: ANALYTICS_WEB_INTERACTION_HERO_BOOKING.NAME, 315 type: ANALYTICS_WEB_INTERACTION_HERO_BOOKING.TYPE, 316 URL: url, 317 }, 318 }, 319 _aramarksports: { 320 web: { 321 webInteraction: { 322 type: 'Booking Submit', 323 }, 324 }, 325 }, 326 }); 327 break; 328 } 329 case ANALYTICS_EVENTS.PRODUCT_COMPARE.name: { 330 const interactionName = eventInfo && typeof eventInfo.name === 'string' 331 ? eventInfo.name 332 : ''; 333 const interactionType = eventInfo && typeof eventInfo.type === 'string' 334 ? eventInfo.type 335 : ''; 336 const interactionUrl = eventInfo && typeof eventInfo.url === 'string' 337 ? eventInfo.url 338 : ''; 339 const productNames = Array.isArray(eventInfo?.productNames) 340 ? eventInfo.productNames 341 .map((entry) => (typeof entry === 'string' ? entry.trim() : '')) 342 .filter(Boolean) 343 : []; 344 345 window.adobeDataLayer.push({ 346 event: eventName, 347 eventType: ANALYTICS_EVENTS.PRODUCT_COMPARE.type, 348 web: { 349 webInteraction: { 350 name: interactionName, 351 type: interactionType, 352 URL: interactionUrl, 353 }, 354 }, 355 productListItems: { 356 name: productNames, 357 }, 358 }); 359 break; 360 } 361 case ANALYTICS_EVENTS.PRODUCT_VIEW.name: { 362 const productName = eventInfo && typeof eventInfo.productName === 'string' 363 ? eventInfo.productName.trim() 364 : ''; 365 if (!productName) break; 366 const productCategory = eventInfo && typeof eventInfo.productCategory === 'string' 367 ? eventInfo.productCategory.trim() 368 : ''; 369 const lastViewedProductName = eventInfo && typeof eventInfo.lastViewedProductName === 'string' 370 ? eventInfo.lastViewedProductName 371 : getLastViewedProductName(); 372 373 window.adobeDataLayer.push({ 374 event: eventName, 375 eventType: ANALYTICS_EVENTS.PRODUCT_VIEW.type, 376 productListItems: { 377 name: productName, 378 productCategories: { 379 categoryName: formatProductCategoryName(productCategory), 380 }, 381 }, 382 _aramarksports: { 383 web: { 384 products: { 385 lastViewedProductName, 386 }, 387 }, 388 }, 389 }); 390 break; 391 } 392 case ANALYTICS_EVENTS.FORM_SUBMIT.name: { 393 const url = eventInfo && typeof eventInfo.url === 'string' ? eventInfo.url : ''; 394 if (!url) break; 395 const pageName = (eventInfo && typeof eventInfo.name === 'string' && eventInfo.name) 396 || (typeof document !== 'undefined' ? document.title : '') 397 || ''; 398 window.adobeDataLayer.push({ 399 event: ANALYTICS_EVENTS.LINK_CLICK.name, 400 eventType: ANALYTICS_EVENTS.FORM_SUBMIT.type, 401 web: { 402 webInteraction: { 403 name: pageName, 404 type: ANALYTICS_WEB_INTERACTION_FORM_SUBMIT.TYPE, 405 URL: url, 406 }, 407 }, 408 _aramarksports: { 409 web: { 410 webInteraction: { 411 type: 'Form Submit', 412 }, 413 }, 414 }, 415 }); 416 break; 417 } 418 case ANALYTICS_EVENTS.SEARCH.name: { 419 const term = eventInfo && typeof eventInfo.term === 'string' ? eventInfo.term.trim() : ''; 420 if (term.length < 3) break; 421 const rawCount = eventInfo?.resultCount; 422 const resultCount = Number.isFinite(rawCount) ? Math.max(0, Math.floor(rawCount)) : 0; 423 window.adobeDataLayer.push({ 424 event: eventName, 425 eventType: ANALYTICS_EVENTS.SEARCH.type, 426 siteSearch: { 427 query: term, 428 numberOfResults: resultCount, 429 }, 430 }); 431 break; 432 } 433 case ANALYTICS_EVENTS.SEARCH_RESULT_CLICK.name: { 434 const term = eventInfo && typeof eventInfo.term === 'string' ? eventInfo.term.trim() : ''; 435 if (term.length < 3) break; 436 const resultURL = eventInfo && typeof eventInfo.resultURL === 'string' 437 ? eventInfo.resultURL.trim() 438 : ''; 439 if (!resultURL) break; 440 const resultName = eventInfo && typeof eventInfo.resultName === 'string' 441 ? eventInfo.resultName.trim() 442 : ''; 443 const rawRank = eventInfo?.resultRank; 444 const resultRank = Number.isFinite(rawRank) 445 ? Math.max(1, Math.floor(rawRank)) 446 : 1; 447 window.adobeDataLayer.push({ 448 event: eventName, 449 eventType: ANALYTICS_EVENTS.SEARCH_RESULT_CLICK.type, 450 siteSearch: { 451 query: term, 452 }, 453 _aramarksports: { 454 web: { 455 search: { 456 resultURL, 457 resultName, 458 resultRank: resultRank.toString(), 459 }, 460 }, 461 }, 462 }); 463 break; 464 } 465 default: 466 break; 467 } 468} 469 470/** Avoid literal `javascript:` in source (eslint no-script-url). */ 471const SCRIPT_PROTOCOL_PREFIX = `${'java'}${'script'}:`; 472 473/** 474 * Visible label text for a link â excludes `.sr-only` supplements (see cards `buildCardCta` 475 * where the full accessible phrase is duplicated in an sr-only span). 476 * @param {HTMLAnchorElement} link 477 * @returns {string} 478 */ 479export function getLinkVisibleText(link) { 480 const clone = link.cloneNode(true); 481 clone.querySelectorAll('.sr-only').forEach((el) => el.remove()); 482 return clone.textContent.replace(/\s+/g, ' ').trim(); 483} 484 485/** 486 * Pushes link-click ACDL data (`component_interaction` / LINK_CLICK) for an anchor. 487 * Skips empty href and `javascript:` URLs. 488 * 489 * @param {HTMLAnchorElement} link 490 * @param {Object} [options] 491 * @param {string} [options.customWebInteractionType] - `_aramarksports.web.webInteraction.type` 492 * @returns {boolean} Whether an analytics event was pushed 493 */ 494export function pushLinkClickAnalytics(link, options = {}) { 495 const rawHref = link.getAttribute('href') || ''; 496 if (!rawHref || rawHref.trim().toLowerCase().startsWith(SCRIPT_PROTOCOL_PREFIX)) { 497 return false;
498 } 499 const url = link.href || rawHref; 500 const name = getLinkVisibleText(link) || link.getAttribute('aria-label') || ''; 501 const eventInfo = { 502 name, 503 url, 504 element: link, 505 }; 506 if (typeof options.customWebInteractionType === 'string') { 507 eventInfo.customWebInteractionType = options.customWebInteractionType; 508 } 509 pushAnalyticsEvent(ANALYTICS_EVENTS.LINK_CLICK.name, eventInfo); 510 return true; 511} 512 513/** 514 * Pushes form-submit ACDL data (`component_interaction` / FORM_SUBMIT) for native form blocks. 515 * 516 * @param {{ url: string, element?: Element, name?: string }} options 517 * @returns {boolean} Whether an analytics event was pushed 518 */ 519export function pushFormSubmitAnalytics({ url, element, name }) { 520 if (!url || typeof url !== 'string') return false; 521 const pageName = (typeof name === 'string' && name) 522 || (typeof document !== 'undefined' ? document.title : '') 523 || ''; 524 pushAnalyticsEvent(ANALYTICS_EVENTS.FORM_SUBMIT.name, { 525 name: pageName, 526 url, 527 element, 528 }); 529 return true; 530} 531 532/** 533 * Pushes search-result-click ACDL data when the user selects a search result. 534 * 535 * @param {{ term: string, resultURL: string, resultName?: string, resultRank?: number }} options 536 * @returns {boolean} Whether a search result click analytics event was requested 537 */ 538export function pushSearchResultClickAnalytics({ 539 term, resultURL, resultName = '', resultRank = 1, 540}) { 541 const normalizedTerm = typeof term === 'string' ? term.trim() : ''; 542 if (normalizedTerm.length < 3) return false; 543 const normalizedURL = typeof resultURL === 'string' ? resultURL.trim() : ''; 544 if (!normalizedURL) return false; 545 const normalizedName = typeof resultName === 'string' ? resultName.trim() : ''; 546 pushAnalyticsEvent(ANALYTICS_EVENTS.SEARCH_RESULT_CLICK.name, { 547 term: normalizedTerm, 548 resultURL: normalizedURL, 549 resultName: normalizedName, 550 resultRank, 551 }); 552 return true; 553} 554 555/** 556 * Pushes product-view ACDL data when the page carries `product_name` metadata. 557 * 558 * @param {{ productName: string, productCategory?: string }} options 559 * @returns {boolean} Whether an analytics event was pushed 560 */ 561export function pushProductViewAnalytics({ productName, productCategory = '' }) { 562 const name = typeof productName === 'string' ? productName.trim() : ''; 563 if (!name) return false; 564 const category = typeof productCategory === 'string' ? productCategory.trim() : ''; 565 pushAnalyticsEvent(ANALYTICS_EVENTS.PRODUCT_VIEW.name, { 566 productName: name, 567 productCategory: category, 568 lastViewedProductName: getLastViewedProductName(), 569 }); 570 return true; 571} 572 573/** 574 * Pushes search-performed ACDL data when the user leaves the search field with a valid term. 575 * 576 * @param {{ term: string, resultCount?: number }} options 577 * @returns {boolean} Whether a search analytics event was requested 578 */ 579export function pushSearchPerformedAnalytics({ term, resultCount = 0 }) { 580 const normalizedTerm = typeof term === 'string' ? term.trim() : ''; 581 if (normalizedTerm.length < 3) return false; 582 pushAnalyticsEvent(ANALYTICS_EVENTS.SEARCH.name, { 583 term: normalizedTerm, 584 resultCount, 585 }); 586 return true; 587} 588 589/** 590 * On product pages, persist `product_name` to sessionStorage when the user leaves. 591 * @param {string} productName 592 */ 593export function attachProductPageLeaveTracking(productName) { 594 const name = typeof productName === 'string' ? productName.trim() : ''; 595 if (!name || productPageLeaveTrackingAttached || typeof window === 'undefined') return; 596 productPageLeaveTrackingAttached = true; 597 window.addEventListener('pagehide', () => { 598 setLastViewedProductName(name); 599 }); 600} 601 602/** RTE blocks that carry prose links outside `.default-content-wrapper` (UE). */ 603const INLINE_DEFAULT_LINK_BLOCK_NAMES = new Set(['text', 'title']); 604 605/** 606 * Delegates link clicks to {@link pushLinkClickAnalytics} for inline / default prose only: 607 * anchors under `.default-content-wrapper`, and under `text` / `title` blocks (by 608 * `data-block-name`). Skips when the anchor sits under a different `.block` than the 609 * listener host (nested block inside RTE or malformed DOM). 610 * 611 * **Fragment mains:** `loadFragment` in `blocks/fragment/fragment.js` calls 612 * `decorateMain(main)` on the fragment root, so this runs there automatically. 613 * 614 * **Header / footer / other chrome:** `decorateMain` only receives `doc.querySelector('main')`. 615 * If those regions need the same behavior, call `attachDefaultContentLinkAnalytics(root)` again 616 * once their DOM is stable (same helper, extra root). 617 * 618 * @param {ParentNode} [root] - Subtree to scan (e.g. `main`); no-op if missing `querySelectorAll`. 619 */ 620export function attachDefaultContentLinkAnalytics(root) { 621 if (!root || typeof root.querySelectorAll !== 'function') return; 622 623 const onClick = (event) => { 624 const a = event.target.closest('a[href]'); 625 if (!a) return; 626 const innerBlock = a.closest('.block'); 627 const host = event.currentTarget; 628 if (innerBlock && innerBlock !== host) return; 629 pushLinkClickAnalytics(a); 630 }; 631 632 root.querySelectorAll('.default-content-wrapper').forEach((el) => { 633 el.addEventListener('click', onClick); 634 }); 635 636 root.querySelectorAll('.block').forEach((block) => { 637 if (INLINE_DEFAULT_LINK_BLOCK_NAMES.has(block.dataset.blockName)) { 638 block.addEventListener('click', onClick); 639 } 640 }); 641}
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.