PageSourceSearch

https://www.impactmojo.in/js/search.js?v=ed523157

js impactmojo.in collected 2026-09-29 07:13:21 UTC 22,923 bytes, 456 lines download raw bytes

1/* ============================================================
2   ImpactMojo Global Search — Ctrl+K / Cmd+K
3   Uses Fuse.js for fuzzy matching across all content
4   ============================================================ */
5(function () {
6  'use strict';
7
8  var FUSE_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js';
9  var INDEX_URL = '/data/search-index.json';
10
11  var fuse = null;
12  var searchData = null;
13  var modal = null;
14  var input = null;
15  var resultsList = null;
16  var selectedIndex = -1;
17  var visible = false;
18
19  var SARGAM_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/Icons/Line/';
20  function sargamImg(name) {
21    return '<img src="' + SARGAM_CDN + name + '.svg" alt="" style="width:18px;height:18px;filter:brightness(0) invert(1);" loading="lazy">';
22  }
23
24  var TYPE_META = {
25    bct:       { icon: sargamImg('si_Fact_check'),  label: 'BCT Technique', color: '#EC4899' },
26    dataverse: { icon: sargamImg('si_Bar_chart'),   label: 'Dataverse',     color: '#6366F1' },
27    course:    { icon: sargamImg('si_Book'),         label: 'Course',        color: '#0EA5E9' },
28    game:      { icon: sargamImg('si_Activity'),     label: 'Game',          color: '#F59E0B' },
29    lab:       { icon: sargamImg('si_Wrench'),          label: 'Studio',           color: '#10B981' },
30    page:      { icon: sargamImg('si_Article'),      label: 'Page',          color: '#8B5CF6' },
31    'book-summary':  { icon: sargamImg('si_Book'),          label: 'Reading Companion', color: '#D97706' },
32    blog:            { icon: sargamImg('si_Article'),       label: 'Blog Post',         color: '#0891B2' },
33    'deep-dive':     { icon: sargamImg('si_Library_books'), label: 'Deep Dive',         color: '#7C3AED' },
34    product:         { icon: sargamImg('si_Bag'),           label: 'Product',           color: '#DB2777' },
35    'practice-pack': { icon: sargamImg('si_Fact_check'),    label: 'Practice Pack',     color: '#059669' },
36    tool:            { icon: sargamImg('si_Wrench'),        label: 'Tool',              color: '#2563EB' },
37    'data-dive':     { icon: sargamImg('si_Bar_chart'),     label: 'Data Note',         color: '#4F46E5' },
38    timeline:        { icon: sargamImg('si_Timer'),         label: 'Timeline',          color: '#B45309' },
39    poster:          { icon: sargamImg('si_Image'),         label: 'Poster',            color: '#BE185D' },
40    reference:       { icon: sargamImg('si_Library_books'), label: 'Reference',         color: '#0D9488' },
41    handout:         { icon: sargamImg('si_File_download'), label: 'Handout',           color: '#65A30D' },
42    challenge:       { icon: sargamImg('si_Flag'),          label: 'Challenge',         color: '#DC2626' },
43    showcase:        { icon: sargamImg('si_Image'),          label: 'Work Sample',       color: '#7C2D12' },
44    special:         { icon: sargamImg('si_Library_books'), label: 'Special',           color: '#9333EA' },
45    'field-radio':   { icon: sargamImg('si_Mic'),           label: 'Field Radio',       color: '#0EA5E9' }
46  };
47
48  /* ---- Load Fuse.js ---- */
49  function loadFuse(cb) {
50    if (window.Fuse) return cb();
51    var s = document.createElement('script');
52    s.src = FUSE_CDN;
53    s.onload = cb;
54    s.onerror = function () { console.warn('ImpactMojo Search: Failed to load Fuse.js'); };
55    document.head.appendChild(s);
56  }
57
58  /* ---- Load search index ---- */
59  function loadIndex(cb) {
60    if (searchData) return cb(searchData);
61    var controller = window.AbortController ? new AbortController() : null;
62    var timeoutId = controller ? setTimeout(function () { controller.abort(); }, 10000) : null;
63    fetch(INDEX_URL, controller ? { signal: controller.signal } : {})
64      .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
65      .then(function (data) { searchData = data; cb(data); })
66      .catch(function (e) { console.warn('ImpactMojo Search: Failed to load index', e); })
67      .finally(function () { if (timeoutId) clearTimeout(timeoutId); });
68  }
69
70  /* ---- Initialize Fuse instance ---- */
71  function initFuse() {
72    loadFuse(function () {
73      loadIndex(function (data) {
74        fuse = new window.Fuse(data, {
75          keys: [
76            { name: 'title', weight: 0.45 },
77            { name: 'description', weight: 0.25 },
78            { name: 'tags', weight: 0.15 },
79            { name: 'category', weight: 0.1 },
80            { name: 'id', weight: 0.05 }
81          ],
82          threshold: 0.35,
83          distance: 200,
84          includeScore: true,
85          minMatchCharLength: 2,
86          limit: 20
87        });
88      });
89    });
90  }
91
92  /* ---- Build modal HTML ---- */
93  function createModal() {
94    modal = document.createElement('div');
95    modal.id = 'impactmojo-search-modal';
96    modal.setAttribute('role', 'dialog');
97    modal.setAttribute('aria-label', 'Search ImpactMojo');
98    modal.innerHTML =
99      '<div class="ims-backdrop"></div>' +
100      '<div class="ims-dialog">' +
101        '<div class="ims-input-row">' +
102          '<svg class="ims-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
103          '<input id="ims-input" type="text" placeholder="Search courses, tools, techniques..." autocomplete="off" aria-label="Search">' +
104          '<kbd class="ims-kbd">ESC</kbd>' +
105        '</div>' +
106        '<div id="ims-results" class="ims-results"></div>' +
107        '<div class="ims-footer">' +
108          '<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>' +
109          '<span><kbd>↵</kbd> open</span>' +
110          '<span><kbd>esc</kbd> close</span>' +
111          '<span class="ims-count" id="ims-count"></span>' +
112        '</div>' +
113      '</div>';
114    document.body.appendChild(modal);
115
116    input = document.getElementById('ims-input');
117    resultsList = document.getElementById('ims-results');
118
119    modal.querySelector('.ims-backdrop').addEventListener('click', closeSearch);
120    input.addEventListener('input', onInput);
121    input.addEventListener('keydown', onKeydown);
122  }
123
124  /* ---- Inject CSS ---- */
125  function injectStyles() {
126    var css = document.createElement('style');
127    css.textContent =
128      '#impactmojo-search-modal{display:none;position:fixed;inset:0;z-index:99999;font-family:"Amaranth","Inter",system-ui,sans-serif}' +
129      '#impactmojo-search-modal.ims-open{display:flex;align-items:flex-start;justify-content:center;padding-top:min(20vh,120px)}' +
130      '.ims-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)}' +
131      '.ims-dialog{position:relative;width:min(640px,90vw);max-height:min(500px,70vh);background:var(--card-bg,#fff);border-radius:16px;box-shadow:0 25px 50px rgba(0,0,0,0.25);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-color,#e2e8f0)}' +
132      '.ims-input-row{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-color,#e2e8f0)}' +
133      '.ims-search-icon{width:20px;height:20px;flex-shrink:0;color:var(--text-muted,#94a3b8)}' +
134      '#ims-input{flex:1;border:none;outline:none;font-size:1rem;background:transparent;color:var(--text-primary,#1e293b);font-family:inherit}' +
135      '#ims-input::placeholder{color:var(--text-muted,#94a3b8)}' +
136      '.ims-kbd{font-size:0.7rem;padding:2px 6px;border-radius:4px;border:1px solid var(--border-color,#e2e8f0);color:var(--text-secondary,#475569);background:var(--surface-color,#f8fafc);font-family:monospace;flex-shrink:0}' +
137      '.ims-results{overflow-y:auto;flex:1;padding:8px}' +
138      '.ims-results::-webkit-scrollbar{width:6px}.ims-results::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}' +
139      '.ims-result{display:flex;align-items:flex-start;gap:12px;padding:10px 12px;border-radius:10px;cursor:pointer;text-decoration:none;color:inherit;transition:background 0.15s}' +
140      '.ims-result:hover,.ims-result.ims-active{background:var(--surface-color,#f1f5f9)}' +
141      '.ims-result-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;color:#fff}' +
142      '.ims-result-body{flex:1;min-width:0}' +
143      '.ims-result-title{font-weight:600;font-size:0.9rem;color:var(--text-primary,#1e293b);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
144      '.ims-result-desc{font-size:0.78rem;color:var(--text-muted,#64748b);margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}' +
145      '.ims-result-meta{display:flex;gap:8px;margin-top:4px}' +
146      '.ims-result-badge{font-size:0.65rem;padding:1px 6px;border-radius:4px;font-weight:600;color:#fff}' +
147      '.ims-footer{display:flex;align-items:center;gap:16px;padding:10px 20px;border-top:1px solid var(--border-color,#e2e8f0);font-size:0.75rem;color:var(--text-muted,#94a3b8)}' +
148      '.ims-footer kbd{font-size:0.7rem;padding:1px 5px;border-radius:3px;border:1px solid var(--border-color,#e2e8f0);background:var(--surface-color,#f8fafc);font-family:monospace;margin:0 2px}' +
149      '.ims-count{margin-left:auto}' +
150      '.ims-empty{padding:40px 20px;text-align:center;color:var(--text-muted,#94a3b8)}
150' +
151      '.ims-empty-icon{font-size:2rem;margin-bottom:8px}' +
152      '.ims-empty-text{font-size:0.9rem}' +
153      '.ims-empty-hint{font-size:0.78rem;margin-top:4px}' +
154      /* Navbar search button */
155      '.ims-nav-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;border:1px solid var(--border-color,#e2e8f0);background:var(--surface-color,#f8fafc);color:var(--text-secondary,#475569);cursor:pointer;font-size:0.82rem;font-family:inherit;transition:all 0.2s;margin-left:8px}' +
156      '.ims-nav-btn:hover{border-color:var(--accent-color,#6366F1);color:var(--text-primary,#1e293b);box-shadow:0 2px 8px rgba(99,102,241,0.15)}' +
157      '.ims-nav-btn svg{width:15px;height:15px}' +
158      '.ims-nav-btn kbd{font-size:0.65rem;padding:1px 5px;border-radius:3px;border:1px solid var(--border-color,#d1d5db);background:var(--card-bg,#fff);font-family:monospace;color:var(--text-secondary,#475569)}' +
159      '@media(max-width:768px){.ims-nav-btn kbd{display:none}.ims-nav-btn{padding:6px 10px}}' +
160      /* Mobile button (#1062). The desktop button lives in .nav-buttons, which
161         css/imx-main.css sets to display:none !important below 768px, so on a
162         phone it was in the DOM at 0x0 and there was no search entry in the
163         hamburger menu either. This one sits in .nav-container, which stays
164         visible, and appears only where the other one disappears. 44px square
165         is the minimum comfortable tap target. */
166      /* order:98 because the mobile bar sequences itself with flex order, not
167         DOM order: .logo-container is 1, .nav-buttons 2, .mobile-menu-toggle
168         99. A button left at the default 0 renders before the logo, which is
169         where this one first landed. 98 puts it immediately left of the
170         hamburger, and margin-left:auto keeps the pair together on the right. */
171      '.ims-nav-btn--m{display:none;align-items:center;justify-content:center;width:44px;height:44px;padding:0;' +
172        'order:98;margin:0 4px 0 auto;border-radius:10px;border:1px solid var(--border-color,#e2e8f0);' +
173        'background:var(--surface-color,#f8fafc);color:var(--text-secondary,#475569);cursor:pointer;flex:0 0 auto}' +
174      '.ims-nav-btn--m svg{width:20px;height:20px}' +
175      '[data-theme="dark"] .ims-nav-btn--m{background:#1e293b;color:#e2e8f0;border-color:#334155}' +
176      '@media(max-width:768px){.ims-nav-btn--m{display:inline-flex}}' +
177      /* --text-secondary is defined in dark mode but --surface-color is not,
178         so the button kept its light fallback background under light text:
179         1.42:1. Pin both sides for dark rather than relying on the vars. */
180      '[data-theme="dark"] .ims-nav-btn{background:#1e293b;color:#e2e8f0;border-color:#334155}';
181    document.head.appendChild(css);
182  }
183
184  /* ---- Field Radio transcripts ----
185     The station's 19 clips carry ~45,000 characters of transcript. They are
186     deliberately NOT in data/search-index.json: that file is fetched on every
187     page load (initFuse runs on DOMContentLoaded below), so carrying them
188     there would charge every visitor on every page for a search few of them
189     run. They are fetched here instead, once, on the first query.
190
191     Why a second Fuse instance rather than another key on the main one: the
192     main index uses Fuse's default location scoring with distance 200, which
193     ranks a match by how near it sits to the start of the field. That is fine
194     for a title and useless for a 4,000-character transcript -- measured, a
195     transcript key on the main index matched nothing that was not in the
196     opening line. ignoreLocation fixes it, and setting it globally moved the
197     top result on a quarter of ordinary queries. So it is set here only. */
198  var trFuse = null, trTried = false;
199
200  function loadTranscripts() {
201    if (trTried) return;
202    trTried = true;
203    fetch('/data/field-radio.json')
204      .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
205      .then(function (d) {
206        var clips = (d && Array.isArray(d.clips) ? d.clips : []).filter(function (c) {
207          return c && c.id && c.transcript;
208        });
209        if (clips.length && window.Fuse) {
210          trFuse = new window.Fuse(clips, {
211            keys: [{ name: 'transcript' }],
212            threshold: 0.3,
213            ignoreLocation: true,
214            minMatchCharLength: 4,
215            includeScore: true,
216            limit: 5
217          });
218        }
219      })
220      .catch(function (e) { console.warn('ImpactMojo Search: transcripts unavailable', e); });
221  }
222
223  /* Clips whose transcript matches, as index entries, minus any already shown. */
224  function transcriptHits(query, already) {
225    if (!trFuse || query.length < 4) return [];
226    var seen = {};
227    already.forEach(function (r) { seen[r.item.id] = true; });
228    var out = [];
229    trFuse.search(query).forEach(function (r) {
230      var entry = null;
231      for (var i = 0; i < searchData.length; i++) {
232        if (searchData[i].id === 'FIELD-RADIO-' + r.item.id) { entry = searchData[i]; break; }
233      }
234      if (entry && !seen[entry.id]) { seen[entry.id] = true; out.push({ item: entry }); }
235    });
236    return out;
237  }
238
239  /* ---- Render results ---- */
240  function renderResults(query) {
241    if (!fuse || !query || query.length < 2) {
242      resultsList.innerHTML =
243        '<div class="ims-empty">' +
244          '<div class="ims-empty-icon">🔍</div>' +
245          '<div class="ims-empty-text">Search ' + (searchData ? searchData.length : 'all') + ' resources across ImpactMojo</div>' +
246          '<div class="ims-empty-hint">Courses, BCT techniques, datasets, games, studios, and more</div>' +
247        '</div>';
248      document.getElementById('ims-count').textContent = '';
249      selectedIndex = -1;
250      return;
251    }
252
253    var results = fuse.search(query, { limit: 15 });
254    // Clips whose spoken words match go after the title/description matches:
255    // a phrase someone half-remembers from a voice note should find it, but
256    // should not outrank a course whose title is what they asked for.
257    results = results.concat(transcriptHits(query, results));
258    if (!results.length) {
259      resultsList.innerHTML =
260        '<div class="ims-empty">' +
261          '<div class="ims-empty-icon">🤔</div>' +
262          '<div class="ims-empty-text">No results for "' + escapeHTML(query) + '"</div>' +
263          '<div class="ims-empty-hint">Try broader terms like "health", "monitoring", or "economics"</div>' +
264        '</div>';
265      document.getElementById('ims-count').textContent = '';
266      selectedIndex = -1;
267      return;
268    }
269
270    var html = '';
271    results.forEach(function (r, i) {
272      var item = r.item;
273      var meta = TYPE_META[item.type] || { icon: sargamImg('si_Article'), label: item.type, color: '#64748b' };
274      html +=
275        '<a class="ims-result' + (i === 0 ? ' ims-active' : '') + '" href="' + escapeHTML(item.url) + '" data-idx="' + i + '"' +
276        (item.url.indexOf('http') === 0 ? ' target="_blank" rel="noopener noreferrer"' : '') + '>' +
277          '<div class="ims-result-icon" style="background:' + meta.color + '">' + meta.icon + '</div>' +
278          '<div class="ims-result-body">' +
279            '<div class="ims-result-title">' + highlightMatch(item.title, query) + '</div>' +
280            '<div class="ims-result-desc">' + escapeHTML(item.description) + '</div>' +
281            '<div class="ims-result-meta">' +
282              '<span class="ims-result-badge" style="background:' + meta.color + '">' + meta.label + '</span>' +
283              (item.category ? '<span class="ims-result-badge" style="background:#64748b">' + escapeHTML(item.category) + '</span>' : '') +
284            '</div>' +
285          '</div>' +
286        '</a>';
287    });
288    resultsList.innerHTML = html;
289    selectedIndex = 0;
290    document.getElementById('ims-count').textContent = results.length + ' result' + (results.length !== 1 ? 's' : '');
291
292    resultsList.querySelectorAll('.ims-result').forEach(function (el) {
293      el.addEventListener('mouseenter', function () {
294        setActive(parseInt(el.dataset.idx));
295      });
296    });
297  }
298
299  function highlightMatch(text, query) {
300    var safe = escapeHTML(text);
301    var q = escapeHTML(query);
302    var regex = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
303    return safe.replace(regex, '<strong style="color:var(--accent-color,#6366F1)">$1</strong>');
304  }
305
306  function escapeHTML(str) {
307    var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
308    return String(str || '').replace(/[&<>"']/g, function (c) { return map[c]; });
309  }
310
311  /* ---- Keyboard navigation ---- */
312  function setActive(idx) {
313    var items = resultsList.querySelectorAll('.ims-result');
314    if (!items.length) return;
315    items.forEach(function (el) { el.classList.remove('ims-active'); });
316    selectedIndex = Math.max(0, Math.min(idx, items.length - 1));
317    items[selectedIndex].classList.add('ims-active');
318    items[selectedIndex].scrollIntoView({ block: 'nearest' });
319  }
320
321  function onKeydown(e) {
322    var items = resultsList.querySelectorAll('.ims-result');
323    if (e.key === 'ArrowDown') {
324      e.preventDefault();
325      setActive(selectedIndex + 1);
326    } else if (e.key === 'ArrowUp') {
327      e.preventDefault();
328      setActive(selectedIndex - 1);
329    } else if (e.key === 'Enter' && items[selectedIndex]) {
330      e.preventDefault();
331      items[selectedIndex].click();
332      closeSearch();
333    } else if (e.key === 'Escape') {
334      e.preventDefault();
335      closeSearch();
336    }
337  }
338
339  function onInput() {
340    renderResults(input.value.trim());
341  }
342
343  /* ---- Open / Close ---- */
344  function openSearch() {
345    if (visible) return;
346    if (!modal) createModal();
347    if (!fuse) initFuse();
348    loadTranscripts();   // fetched when someone opens search, not on every page load
349    visible = true;
350    modal.classList.add('ims-open');
351    input.value = '';
352    renderResults('');
353    setTimeout(function () { input.focus(); }, 50);
354    document.body.style.overflow = 'hidden';
355  }
356
357  function closeSearch() {
358    if (!visible) return;
359    visible = false;
360    modal.classList.remove('ims-open');
361    document.body.style.overflow = '';
362  }
363
364  /* ---- Global keyboard shortcut ---- */
365  document.addEventListener('keydown', function (e) {
366    if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
367      e.preventDefault();
368      if (visible) closeSearch(); else openSearch();
369    }
370    if (e.key === '/' && !visible && !isInputFocused()) {
371      e.preventDefault();
372      openSearch();
373    }
374  });
375
376  function isInputFocused() {
377    var el = document.activeElement;
378    return el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
379  }
380
381  var ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
382             '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
383
384  function makeButton(cls, label, inner) {
385    var btn = document.createElement('button');
386    btn.type = 'button';
387    btn.className = cls;
388    btn.setAttribute('aria-label', label);
389    btn.innerHTML = inner;
390    btn.addEventListener('click', function (e) { e.preventDefault(); openSearch(); });
391    return btn;
392  }
393
394  /* ---- Inject nav button ---- */
395  function injectNavButton() {
396    /* Pages that load the shared top bar already have .im-sc-search there, and
397       that one keeps its icon on a narrow screen. Two search buttons in one bar
398       is worse than one. */
399    if (document.querySelector('.im-sc-search')) return;
400
401    var btn = makeButton('ims-nav-btn', 'Search (Ctrl+K)', ICON + 'Search' + '<kbd>⌘K</kbd>');
402
403    /* Place in nav-buttons area (next to theme selector / auth buttons) for prominence */
404    var navButtons = document.querySelector('.nav-buttons');
405    if (navButtons) {
406      var themeSelector = navButtons.querySelector('.theme-selector');
407      if (themeSelector) {
408        navButtons.insertBefore(btn, themeSelector);
409      } else {
410        navButtons.insertBefore(btn, navButtons.firstChild);
411      }
412      injectMobileButton();
413      return;
414    }
415
416    /* Fallback: append to nav container. This one is not inside .nav-buttons,
417       so it stays visible on a phone and needs no companion. */
418    var nav = document.querySelector('.nav-with-badge') || document.querySelector('.nav-container');
419    if (nav) {
420      nav.style.display = 'flex';
421      nav.style.alignItems = 'center';
422      nav.appendChild(btn);
423    }
424  }
425
426  /* ---- Mobile companion (#1062) ----
427     .nav-buttons is display:none below 768px, so the button above cannot be
428     reached on a phone; the hamburger menu carries no search entry either, and
429     Ctrl+K is not available to someone holding a phone. This puts an icon-only
430     button in .nav-container, which stays laid out at every width, next to the
431     hamburger. CSS shows it only below the same breakpoint, so the two are
432     never both on screen. */
433  function injectMobileButton() {
434    var container = document.querySelector('.nav-container');
435    if (!container || container.querySelector('.ims-nav-btn--m')) return;
436    var btn = makeButton('ims-nav-btn ims-nav-btn--m', 'Search', ICON);
437    var toggle = container.querySelector('.mobile-menu-toggle, #mobile-menu-btn');
438    if (toggle) container.insertBefore(btn, toggle);
439    else container.appendChild(btn);
440  }
441
442  /* ---- Init ---- */
443  injectStyles();
444
445  if (document.readyState === 'loading') {
446    document.addEventListener('DOMContentLoaded', function () {
447      injectNavButton();
448      initFuse();
449    });
450  } else {
451    injectNavButton();
452    initFuse();
453  }
454
455  window.ImpactMojoSearch = { open: openSearch, close: closeSearch };
456})();

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.