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,'&').replace(/</g,'<').replace(/>/g,'>'); 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 + '¬es=' + 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.