PageSourceSearch

https://glideslope.ai/post.js

js glideslope.ai collected 2026-09-29 03:52:27 UTC 26,010 bytes, 574 lines download raw bytes

1/* ==========================================================================
2   post.js — Glideslope AI article detail page JavaScript
3   Extracted from post.php inline <script> block.
4
5   PHP data is injected via window.POST_CONFIG (set in post.php before this
6   script loads). All PHP → JS communication goes through that object.
7   ========================================================================== */
8
9document.addEventListener('DOMContentLoaded', function () {
10
11    /* Shorthand config reference */
12    var C = window.POST_CONFIG || {};
13
14    /* -------------------------------------------------------------------
15       UTILITIES
16    ------------------------------------------------------------------- */
17    function qs(selector) { return document.querySelector(selector); }
18
19    function setDisabled(el, on) {
20        if (!el) return;
21        el.disabled = !!on;
22        el.setAttribute('aria-disabled', on ? 'true' : 'false');
23        el.style.pointerEvents = on ? 'none' : '';
24        el.style.opacity = on ? '0.6' : '';
25    }
26
27    function setBusy(btn, busyText) {
28        if (!btn) return function () {};
29        var old = btn.textContent;
30        btn.textContent = busyText;
31        setDisabled(btn, true);
32        return function () { btn.textContent = old; setDisabled(btn, false); };
33    }
34
35    function autoResize(textarea) {
36        if (!textarea) return;
37        textarea.style.height = 'auto';
38        textarea.style.height = textarea.scrollHeight + 'px';
39    }
40
41    /* -------------------------------------------------------------------
42       PULSE MARKDOWN RENDERER
43       Converts the AI's markdown output to clean HTML.
44       Handles: ## headings, **bold**, *italic*, _italic_, bullet lists.
45    ------------------------------------------------------------------- */
46    function renderPulse(text) {
47        if (!text) return '';
48        var lines = text.split('\n');
49        var html = '';
50        var inList = false;
51
52        lines.forEach(function(line) {
53            // ## Heading → <strong><u>Heading</u></strong>
54            var heading = line.match(/^#{1,3}\s+(.+)$/);
55            if (heading) {
56                if (inList) { html += '</ul>'; inList = false; }
57                html += '<p><strong><u>' + formatInline(heading[1]) + '</u></strong></p>';
58                return;
59            }
60
61            // Bullet list item (-, *, •)
62            var bullet = line.match(/^[\-\*•]\s+(.+)$/);
63            if (bullet) {
64                if (!inList) { html += '<ul>'; inList = true; }
65                html += '<li>' + formatInline(bullet[1]) + '</li>';
66                return;
67            }
68
69            // Close list if open
70            if (inList) { html += '</ul>'; inList = false; }
71
72            // Blank line → paragraph break
73            if (line.trim() === '') {
74                html += '<br>';
75                return;
76            }
77
78            // Normal line
79            html += '<p>' + formatInline(line) + '</p>';
80        });
81
82        if (inList) html += '</ul>';
83        return html;
84    }
85
86    // Handle inline formatting: **bold**, *italic*, _italic_
87    function formatInline(text) {
88        // Escape HTML first so user content can't inject tags
89        text = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
90        // **bold**
91        text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
92        // *italic* or _italic_
93        text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
94        text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
95        return text;
96    }
97
98
99    /* -------------------------------------------------------------------
100       SNAP: Open PostSnap modal (canvas-based, pulse/snap.js style)
101    ------------------------------------------------------------------- */
102    function openSnapModal() {
103        var publicPulseEl = qs('#publicPulse');
104        var pulseText = '';
105        if (publicPulseEl) {
106            var t = publicPulseEl.textContent.trim();
107            if (t && !t.includes('Pulse analysis not available yet') && !t.includes('Failed to get Pulse')) {
108                pulseText = t;
109            }
110        }
111        // Fall back to PHP-injected saved pulse if the DOM element isn't populated yet
112        if (!pulseText && C.savedPulse) {
113            pulseText = C.savedPulse;
114        }
115
116        if (window.PostSnap) {
117            window.PostSnap.open({
118                title:           C.title           || '',
119                description:     C.description     || '',
120                source:          C.source          || '',
121                pubDate:         C.pubDate         || '',
122                slug:            C.slug            || '',
123                link:            C.link            || '',   // source article URL
124                featuredImageUrl: C.featuredImageUrl || '',
125                sentimentLabel:  C.sentimentLabel  || 'Neutral',
126                sentimentScore:  C.sentimentScore  || '0',
127                sentimentColor:  C.sentimentColor  || '#6b7280',
128                sentimentPosPct: (C.sentimentPosPct != null && C.sentimentPosPct !== '') ? Number(C.sentimentPosPct) : 50,
129                pulseText:       pulseText,
130                notesText:       C.articleNotes    || '',
131                notesUser:       C.articleUsername || '',
132                isLoggedIn:      !!C.isLoggedIn,
133            });
134        } else {
135            console.error('PostSnap not loaded');
136        }
137    }
138
139    /* Single Snap button listener */
140    var $snapBtn = qs('#snapButton');
141    if ($snapBtn) $snapBtn.addEventListener('click', openSnapModal);
142
143
144    /* -------------------------------------------------------------------
145       NOTES: Save (debounced)
146    ------------------------------------------------------------------- */
147    var debounceTimer;
148    function debounceSaveNotes(notes) {
149        clearTimeout(debounceTimer);
150        debounceTimer = setTimeout(function () { saveNotes(notes); }, 600);
151    }
152
153    function saveNotes(notes) {
154        if (!C.articleId)  { console.error('Missing article ID'); return; }
155        if (!C.csrfToken)  { console.error('Missing CSRF token'); return; }
156        fetch('/update_bookmark.php', {
157            method: 'POST',
158            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
159            body: 'article_id=' + encodeURIComponent(C.articleId)
160                + '&notes='      + encodeURIComponent(notes)
161                + '&csrf_token=' + encodeURIComponent(C.csrfToken)
162        }).then(function (r) {
163            if (!r.ok) { console.error('Save notes failed:', r.status); }
164            C.articleNotes = notes;
165            return r.text();
166        }).then(function (d) { console.log('Save notes:', d); })
167          .catch(function (e) { console.error('Save notes network error:', e); });
168    }
169
170
171    /* -------------------------------------------------------------------
172       NOTES: Init bookmark notes area
173    ------------------------------------------------------------------- */
174    function initializeBookmarkLogic() {
175        var ta        = qs('#bookmark-notes');
176        var container = qs('.bookmark-container');
177        if (ta && container) {
178            if (container.classList.contains('active')) {
179                autoResize(ta);
180                container.style.display = 'block';
181            } else {
182                container.style.display = 'none';
183            }
184            ta.addEventListener('input', function () {
185                autoResize(this);
186                debounceSaveNotes(this.value);
187            });
188        }
189        var getPulseNotesBtn = qs('#getPulse');
190        if (getPulseNotesBtn) getPulseNotesBtn.addEventListener('click', handleGetPulseForNotes);
191    }
192
193
194    /* -------------------------------------------------------------------
195       NOTES: Inject Pulse into notes
196    ------------------------------------------------------------------- */
197    function handleGetPulseForNotes() {
198        var ta  = qs('#bookmark-notes');
199        var btn = qs('#getPulse');
200        if (!ta) return;
201        if (btn) setDisabled(btn, true);
202        var originalNotes = ta.value;
203        ta.value = 'Getting the pulse...';
204        autoResize(ta);
205        fetch('/pulse_api.php', {
206            method: 'POST',
207            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
208            body: 'title='        + encodeURIComponent(C.title || '')
209                + '&description=' + encodeURIComponent(C.description || '')
210                + '&link='        + encodeURIComponent(C.link || '')
211        }).then(function (r) {
212            if (!r.ok) throw new Error('Pulse API: ' + r.statusText);
213            return r.text();
214        }).then(function (pulseText) {
215            pulseText = (pulseText || '').trim();
216            var newNotes = originalNotes ? pulseText + '\n\n---\n\n' + originalNotes : pulseText;
217            ta.value = newNotes;
218            autoResize(ta);
219            saveNotes(newNotes);
220            var publicPulseEl = qs('#publicPulse');
221            if (publicPulseEl) publicPulseEl.innerHTML = renderPulse(pulseText);
222            var getPulseCommonBtn = qs('#getPulseCommon');
223            if (getPulseCommonBtn) getPulseCommonBtn.style.display = 'none';
224        }).catch(function (err) {
225            console.error('Pulse for notes error:', err);
226            ta.value = originalNotes + '\n\n(Failed to get Pulse)';
227            autoResize(ta);
228        }).then(function () {
229            if (btn) setDisabled(btn, false);
230        });
231    }
232
233
234    /* -------------------------------------------------------------------
235       BOOKMARK: Add / Remove
236    ------------------------------------------------------------------- */
237    function addBookmark() {
238        var container = qs('.bookmark-container');
239        if (!container || !C.articleId) return;
240        
241        // FIX 1: Removed '.action-bar' from the selector
242        var addBtn = qs('.bookmark-btn'); 
243        if (addBtn) setDisabled(addBtn, true);
244        
245        fetch('/add_bookmark.php?article_id=' + encodeURIComponent(C.articleId))
246        .then(function (r) { if (!r.ok) throw new Error('Add bookmark failed'); return r.text(); })
247        .then(function () {
248            container.style.display = 'block';
249            container.classList.add('active');
250            
251            var currentBtn = qs('.bookmark-btn');
252            if (currentBtn) {
253                // FIX 2: Insert the exact HTML from post.php to maintain styling and SVG icons
254                currentBtn.outerHTML = '<button class="remove-bookmark-btn gs-btn gs-btn--danger" onclick="removeBookmark()"><span class="gs-btn__icon"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 1.5L7.3 4.2L10.3 4.6L8.15 6.7L8.7 9.7L6 8.3L3.3 9.7L3.85 6.7L1.7 4.6L4.7 4.2L6 1.5Z" stroke="currentColor" stroke-width="1.3" fill="currentColor" stroke-linejoin="round"/></svg></span><span class="gs-btn__text">Remove Bookmark</span></button>';
255            }
256            
257            var ta = qs('#bookmark-notes');
258            if (ta) autoResize(ta);
259        })
260        .catch(function (e) { console.error('addBookmark:', e); if (addBtn) setDisabled(addBtn, false); });
261    }
262
263    function removeBookmark() {
264        if (typeof createConfirm === 'function') {
265            createConfirm('Remove this bookmark? Your notes will also be deleted.', function () { proceedRemoveBookmark(); });
266        } else if (confirm('Remove this bookmark? Your notes will also be deleted.')) {
267            proceedRemoveBookmark();
268        }
269    }
270
271    function proceedRemoveBookmark() {
272        var container = qs('.bookmark-container');
273        if (!container || !C.articleId) return;
274        
275        // FIX 1: Removed '.action-bar' from the selector
276        var removeBtn = qs('.remove-bookmark-btn'); 
277        if (removeBtn) setDisabled(removeBtn, true);
278        
279        fetch('/remove_bookmark.php?article_id=' + encodeURIComponent(C.articleId))
280        .then(function (r) { if (!r.ok) throw new Error('Remove bookmark failed'); return r.text(); })
281        .then(function () {
282            container.style.display = 'none';
283            container.classList.remove('active');
284            
285            var currentBtn = qs('.remove-bookmark-btn');
286            if (currentBtn) {
287                // FIX 2: Insert the exact HTML from post.php to maintain styling and SVG icons
288                currentBtn.outerHTML = '<button class="bookmark-btn gs-btn" onclick="addBookmark()"><span class="gs-btn__icon"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 1.5L7.3 4.2L10.3 4.6L8.15 6.7L8.7 9.7L6 8.3L3.3 9.7L3.85 6.7L1.7 4.6L4.7 4.2L6 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg></span><span class="gs-btn__text">Add Bookmark</span></button>';
289            }
290            
291            var ta = qs('#bookmark-notes');
292            if (ta) ta.value = '';
293            C.articleNotes = '';
294        })
295        .catch(function (e) { console.error('removeBookmark:', e); if (removeBtn) setDisabled(removeBtn, false); });
296    }
297
298    /* Expose bookmark functions for onclick= attributes */
299    window.addBookmark    = addBookmark;
300    window.removeBookmark = removeBookmark;
301
302
303    /* -------------------------------------------------------------------
304       SHARE MODAL
305    ------------------------------------------------------------------- */
306    (function initShareModal() {
307        var toggleBtn = qs('#shareToggleBtn');
308        var overlay   = qs('#shareModalOverlay');
309        var closeBtn  = qs('#shareModalClose');
310        if (!toggleBtn || !overlay) return;
311
312        /* ── helpers ── */
313        function getShareUrl()   { return window.location.href; }
314        function getShareTitle() { return C.title || document.title || 'Check out this article'; }
315
316        /* ── open modal ── */
317        function openModal() {
318            var url   = getShareUrl();
319            var title = getShareTitle();
320            var text  = title + '\n\n' + url;
321
322            /* Populate URL strip */
323            var urlLabel = qs('#shareModalUrl');
324            if (urlLabel) urlLabel.textContent = url;
325
326            /* X / Twitter */
327            var xEl = qs('#shareX');
328            if (xEl) {
329                var xText = title + '\n\n' + url;
330                if (C.sourceHandle) xText += ' via ' + C.sourceHandle;
331                xText += '\n#GlideslopeAI #News';
332                xEl.href = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(xText);
333            }
334
335            /* Threads */
336            var threadsEl = qs('#shareThreads');
337            if (threadsEl) {
338                threadsEl.href = 'https://www.threads.net/intent/post?text=' + encodeURIComponent(text);
339            }
340
341            /* Facebook */
342            var fbEl = qs('#shareFacebook');
343            if (fbEl) {
344                fbEl.href = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url);
345            }
346
347            /* LinkedIn */
348            var liEl = qs('#shareLinkedIn');
349            if (liEl) {
350                liEl.href = 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(url);
351            }
352
353            /* Email */
354            var emailEl = qs('#shareEmail');
355            if (emailEl) {
356                var subject = encodeURIComponent('Interesting article: ' + title);
357                var body    = encodeURIComponent(title + '\n\n' + url + '\n\nShared via Glideslope.ai');
358                emailEl.href = 'mailto:?subject=' + subject + '&body=' + body;
359            }
360
361            /* Apps tile — hide if Web Share API unavailable */
362            var appsEl = qs('#shareApps');
363            if (appsEl) appsEl.style.display = navigator.share ? '' : 'none';
364
365            overlay.classList.add('gs-share-overlay--open');
366            overlay.removeAttribute('aria-hidden');
367            toggleBtn.setAttribute('aria-expanded', 'true');
368            document.body.style.overflow = 'hidden';
369        }
370
371        /* ── close modal ── */
372        function closeModal() {
373            overlay.classList.remove('gs-share-overlay--open');
374            overlay.setAttribute('aria-hidden', 'true');
375            toggleBtn.setAttribute('aria-expanded', 'false');
376            document.body.style.overflow = '';
377        }
378
379        /* ── Wire events ── */
380        toggleBtn.addEventListener('click', function () { openModal(); });
381        if (closeBtn) closeBtn.addEventListener('click', function () { closeModal(); });
382
383        /* Click outside modal card closes it */
384        overlay.addEventListener('click', function (e) {
385            if (e.target === overlay) closeModal();
386        });
387
388        /* Escape key */
389        document.addEventListener('keydown', function (e) {
390            if (e.key === 'Escape' && overlay.classList.contains('gs-share-overlay--open')) {
391                closeModal();
392            }
393        });
394
395        /* ── Copy link ── */
396        var copyBtn   = qs('#shareModalCopyBtn');
397        var copyIcon  = qs('#shareModalCopyIcon');
398        var copyLabel = qs('#shareModalCopyLabel');
399        var checkSvg  = '<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M2.5 7L5.5 10L11.5 4" stroke="#27ae60" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
400        var linkSvg   = '<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M5.5 8.5L8.5 5.5M6 4L6.7 3.3C7.87 2.13 9.8 2.13 10.97 3.3C12.14 4.47 12.14 6.4 10.97 7.57L10.3 8.24" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M8 10L7.33 10.67C6.16 11.84 4.23 11.84 3.06 10.67C1.89 9.5 1.89 7.57 3.06 6.4L3.73 5.73" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
401
402        function onCopySuccess() {
403            if (copyBtn)   copyBtn.classList.add('gs-copied');
404            if (copyIcon)  copyIcon.innerHTML = checkSvg;
405            if (copyLabel) copyLabel.textContent = 'Copied!';
406            setTimeout(function () {
407                if (copyBtn)   copyBtn.classList.remove('gs-copied');
408                if (copyIcon)  copyIcon.innerHTML = linkSvg;
409                if (copyLabel) copyLabel.textContent = 'Copy';
410                closeModal();
411            }, 1500);
412        }
413
414        function doCopy() {
415            var url = getShareUrl();
416            if (navigator.clipboard && navigator.clipboard.writeText) {
417                navigator.clipboard.writeText(url).then(onCopySuccess).catch(fallback);
418            } else { fallback(); }
419            function fallback() {
420                var ta = document.createElement('textarea');
421                ta.value = url;
422                ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
423                document.body.appendChild(ta);
424                ta.focus(); ta.select();
425                try { document.execCommand('copy'); } catch (e) { /* ignore */ }
426                document.body.removeChild(ta);
427                onCopySuccess();
428            }
429        }
430        if (copyBtn) copyBtn.addEventListener('click', doCopy);
431
432        /* ── Share to Apps (Web Share API) ── */
433        var appsBtn = qs('#shareApps');
434        if (appsBtn) {
435            appsBtn.addEventListener('click', function () {
436                closeModal();
437                navigator.share({
438                    title: getShareTitle(),
439                    text:  getShareTitle(),
440                    url:   getShareUrl()
441                }).catch(function (err) {
442                    if (err.name !== 'AbortError') console.warn('Web Share error:', err);
443                });
444            });
445        }
446    })();
447
448
449    /* -------------------------------------------------------------------
450       PULSE AI: Get Pulse button (main action bar)
451    ------------------------------------------------------------------- */
452    if (C.isLoggedIn) {
453        var getPulseCommonBtn  = qs('#getPulseCommon');
454        var publicPulseDisplay = qs('#publicPulse');
455
456        if (getPulseCommonBtn && publicPulseDisplay) {
457            var hasPulseInitially = publicPulseDisplay.textContent.trim()
458                && !publicPulseDisplay.textContent.includes('Pulse analysis not available yet');
459            if (hasPulseInitially) getPulseCommonBtn.style.display = 'none';
460
461            getPulseCommonBtn.addEventListener('click', function () {
462                var restore = setBusy(getPulseCommonBtn, 'Getting Pulse...');
463                fetch('/pulse_api.php', {
464                    method: 'POST',
465                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
466                    body: 'title='        + encodeURIComponent(C.title || '')
467                        + '&description=' + encodeURIComponent(C.description || '')
468                        + '&link='        + encodeURIComponent(C.link || '')
469                }).then(function (r) {
470                    return r.text().then(function (body) {
471                        if (!r.ok || body.indexOf('PULSE_ERROR:') === 0) {
472                            throw new Error(body || ('HTTP ' + r.status));
473                        }
474                        return body;
475                    });
476                }).then(function (pulse) {
477                    pulse = (pulse || '').trim();
478                    if (!pulse) throw new Error('Empty response from Pulse API');
479                    return fetch('/save_pulse.php', {
480                        method: 'POST',
481                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
482                        body: 'article_id=' + encodeURIComponent(C.articleId || '') + '&pulse=' + encodeURIComponent(pulse)
483                    }).then(function () { return pulse; });
484                }).then(function (pulse) {
485                    publicPulseDisplay.innerHTML = renderPulse(pulse);
486                    C.savedPulse = pulse; // keep in sync for snap modal
487                    getPulseCommonBtn.style.display = 'none';
488                }).catch(function (e) {
489                    console.error('getPulseCommon:', e);
490                    var msg = (e.message || '').replace('PULSE_ERROR: ', '');
491                    publicPulseDisplay.innerHTML = '<em style="color:var(--sentiment-bearish)">Pulse error: ' + msg + '</em>';
492                }
492).then(function () { restore(); });
493            });
494        }
495    }
496
497
498    /* -------------------------------------------------------------------
499       LAZY IMAGE LOADING
500    ------------------------------------------------------------------- */
501    function showImagePlaceholder(loaderItem) {
502        var container = loaderItem.classList.contains('topic-item')
503            ? loaderItem.querySelector('.topic-image-container')
504            : loaderItem;
505        if (!container || container.querySelector('.placeholder')) return;
506        var img = document.createElement('img');
507        img.src = '/gsai_mini.png';
508        img.alt = 'Glideslope.ai';
509        img.className = (loaderItem.dataset.imgClass || '') + ' placeholder';
510        container.appendChild(img);
511        loaderItem.dataset.hasImage = 'true';
512        container.classList.remove('loading');
513    }
514
515    function fetchImage(loaderItem) {
516        var articleId = loaderItem.dataset.articleId;
517        var scrapeUrl = loaderItem.dataset.scrapeUrl;
518        var container = loaderItem.classList.contains('topic-item')
519            ? loaderItem.querySelector('.topic-image-container')
520            : loaderItem;
521        if (!articleId || !scrapeUrl || !container) return;
522        container.classList.add('loading');
523        fetch('/fetch_image.php', {
524            method: 'POST',
525            headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
526            body: JSON.stringify({ article_id: articleId, url: scrapeUrl })
527        }).then(function (r) {
528            if (!r.ok) throw new Error('Network error');
529            return r.json();
530        }).then(function (data) {
531            if (data.success && data.imageUrl) {
532                var img = document.createElement('img');
533                img.src = data.imageUrl;
534                img.alt = (loaderItem.querySelector('.topic-blurb, .lt-title, .story-title') || {}).textContent || 'Article Image';
535                img.className = loaderItem.dataset.imgClass || '';
536                img.onload = function () {
537                    container.appendChild(img);
538                    loaderItem.dataset.hasImage = 'true';
539                    container.classList.remove('loading');
540                };
541                img.onerror = function () { showImagePlaceholder(loaderItem); };
542            } else { showImagePlaceholder(loaderItem); }
543        }).catch(function () { showImagePlaceholder(loaderItem); });
544    }
545
546    if ('IntersectionObserver' in window) {
547        var itemsToLoad = document.querySelectorAll('.js-image-loader[data-has-image="false"]');
548        if (itemsToLoad.length) {
549            var observer = new IntersectionObserver(function (entries, obs) {
550                entries.forEach(function (entry) {
551                    if (entry.isIntersecting) { fetchImage(entry.target); obs.unobserve(entry.target); }
552                });
553            }, { rootMargin: '100px 0px' });
554            itemsToLoad.forEach(function (item) { observer.observe(item); });
555        }
556    }
557
558
559    /* -------------------------------------------------------------------
560       INIT
561    ------------------------------------------------------------------- */
562    initializeBookmarkLogic();
563
564    /* Render any server-side saved pulse text through the markdown renderer */
565    var pulseRawScript = document.getElementById('pulse-raw-data');
566    var savedPulseEl   = qs('#publicPulse');
567    if (pulseRawScript && savedPulseEl) {
568        try {
569            var rawPulse = JSON.parse(pulseRawScript.textContent);
570            if (rawPulse) { savedPulseEl.innerHTML = renderPulse(rawPulse); }
571        } catch (e) { console.error('pulse-raw-data parse error:', e); }
572    }
573
574}); /* DOMContentLoaded */

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.