'use strict'; /** * paste_view.js — Logic for the paste view page. * Depends on: crypto.js (PasteCrypto), app.js (copyToClipboard) * * Reads the site name from so no Jinja injection * is needed in this external file. */ let _decryptedPaste = null; document.addEventListener('DOMContentLoaded', async () => { let rawPayload; try { const island = document.getElementById('encryptedPayload'); if (!island) return; // Not a paste view page rawPayload = JSON.parse(island.textContent); } catch (e) { showError('Bad Data', 'Could not read the paste payload.'); return; } // Detect format from the data itself. // Encrypted pastes: "base64url:base64url" string. // Plaintext pastes: a JSON object. const isEncrypted = typeof rawPayload === 'string' && /^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$/.test(rawPayload); if (isEncrypted) { const keyBase64 = window.location.hash.slice(1); if (!keyBase64) { showError('No Key', 'The decryption key is missing from the URL. ' + 'Use the full link including the # part.'); return; } try { const key = await PasteCrypto.importKey(keyBase64); const plaintext = await PasteCrypto.decrypt(rawPayload, key); _decryptedPaste = JSON.parse(plaintext); } catch (e) { showError('Decryption Failed', 'Wrong key or tampered data.'); return; } } else { // Plaintext paste — rawPayload is already the parsed JSON object. try { _decryptedPaste = (typeof rawPayload === 'object') ? rawPayload : JSON.parse(rawPayload); } catch (e) { showError('Bad Data', 'Could not parse paste data.'); return; } } renderPaste(_decryptedPaste); initPasteActions(); initLineHighlight(); // register Prism hook before initLineNumbers triggers Prism initLineNumbers(); initDeletion(); }); function initPasteActions() { const rawBtn = document.getElementById('rawBtn'); const copyBtn = document.getElementById('copyBtn'); const downloadBtn = document.getElementById('downloadBtn'); if (rawBtn) rawBtn.addEventListener('click', rawView); if (copyBtn) copyBtn.addEventListener('click', copyPaste); if (downloadBtn) downloadBtn.addEventListener('click', downloadPaste); } function initLineNumbers() { const toggle = document.getElementById('lineNoToggle'); const viewPre = document.getElementById('viewPre'); if (!toggle || !viewPre) return; // Load preference from localStorage (default to checked) const stored = localStorage.getItem('show_line_numbers'); const isEnabled = (stored === null) ? true : (stored === 'true'); toggle.checked = isEnabled; const updateLines = () => { const checked = toggle.checked; if (checked) { viewPre.classList.add('line-numbers'); } else { viewPre.classList.remove('line-numbers'); const existing = viewPre.querySelector('.line-numbers-rows'); if (existing) existing.remove(); } localStorage.setItem('show_line_numbers', checked); // Always re-highlight so Prism's line-numbers plugin runs (works for plain text too) const code = document.getElementById('codeBlock'); if (code) { Prism.highlightElement(code); } }; toggle.addEventListener('change', updateLines); // Apply initial state (calls Prism so line numbers render without needing a toggle) updateLines(); } function renderPaste(paste) { const siteName = document.querySelector('meta[name="site-name"]')?.content || ''; const title = paste.title || 'Untitled'; document.title = title + (siteName ? ' \u2014 ' + siteName : ''); document.getElementById('navPasteTitle').textContent = title; const lang = paste.language || 'text'; const prismLangMap = { text: false, html: 'markup', xml: 'markup', docker: 'docker' }; const prismLang = (lang in prismLangMap) ? prismLangMap[lang] : lang; const codeBlock = document.getElementById('codeBlock'); const viewPre = document.getElementById('viewPre'); codeBlock.textContent = paste.content || ''; if (prismLang) { codeBlock.className = 'language-' + prismLang; viewPre.className = 'language-' + prismLang; Prism.highlightElement(codeBlock); } document.getElementById('viewFull').style.display = 'block'; } function showError(title, detail) { document.getElementById('errorTitle').textContent = title; document.getElementById('errorDetail').textContent = detail; document.getElementById('errorState').style.display = 'block'; } function rawView() { const key = window.location.hash; const url = window.location.href.split('?')[0].split('#')[0] + '/raw' + key; window.open(url, '_blank'); } async function copyPaste() { if (!_decryptedPaste) return; const ok = await copyToClipboard(_decryptedPaste.content); const btn = document.getElementById('copyBtn'); if (btn) { const t = btn.textContent; btn.textContent = ok ? 'Copied!' : 'Failed'; setTimeout(() => btn.textContent = t, 1500); } } function downloadPaste() { if (!_decryptedPaste) return; const { title = 'untitled', content = '', language = 'text' } = _decryptedPaste; const extMap = { javascript: '.js', typescript: '.ts', python: '.py', java: '.java', c: '.c', cpp: '.cpp', csharp: '.cs', html: '.html', css: '.css', scss: '.scss', sql: '.sql', json: '.json', yaml: '.yaml',xml: '.xml', bash: '.sh', powershell: '.ps1', php: '.php', ruby: '.rb', go: '.go', rust: '.rs', swift: '.swift',kotlin: '.kt', markdown:'.md', diff: '.diff', docker: '', nginx: '.conf', toml: '.toml', ini: '.ini', }; const filename = title.replace(/[^a-z0-9.\-]/gi, '_') + (extMap[language] ?? '.txt'); const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = Object.assign(document.createElement('a'), { href: url, download: filename }); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function initDeletion() { const pasteId = window.location.pathname.split('/').pop(); const token = localStorage.getItem('del_' + pasteId); const deleteBtn = document.getElementById('deleteBtn'); if (token && deleteBtn) { deleteBtn.style.display = 'inline-block'; deleteBtn.addEventListener('click', async () => { if (!confirm('Are you sure you want to delete this paste? This action cannot be undone.')) return; deleteBtn.disabled = true; deleteBtn.textContent = '...'; try { const resp = await fetch('/api/delete/' + pasteId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) }); const result = await resp.json(); if (result.success) { localStorage.removeItem('del_' + pasteId); window.location.href = '/'; } else { alert('Error: ' + (result.error || 'Unknown error')); deleteBtn.disabled = false; deleteBtn.textContent = 'Delete'; } } catch (err) { console.error(err); alert('Failed to delete paste. Connection error.'); deleteBtn.disabled = false; deleteBtn.textContent = 'Delete'; } }); } } // ── Line highlight / linking ────────────────────────────────────────────────── let _hlAnchor = null; // anchor line for shift-click range let _hlStart = null; let _hlEnd = null; let _hlScrolled = false; function initLineHighlight() { // Parse ?L=5 or ?L=5-12 from the URL const lParam = new URLSearchParams(window.location.search).get('L'); if (lParam) { const m = lParam.match(/^(\d+)(?:-(\d+))?$/); if (m) { _hlStart = parseInt(m[1], 10); _hlEnd = m[2] ? parseInt(m[2], 10) : _hlStart; _hlAnchor = _hlStart; } } // After every Prism highlight, wire gutter clicks and redraw highlight bars. // initLineNumbers() calls Prism, which fires this hook. Prism.hooks.add('complete', function (env) { if (env.element && env.element.id === 'codeBlock') { _wireLineClicks(); if (_hlStart !== null) { _drawHighlight(_hlStart, _hlEnd); if (!_hlScrolled) { _scrollToLine(_hlStart); _hlScrolled = true; } } } }); } function _wireLineClicks() { const viewPre = document.getElementById('viewPre'); if (!viewPre) return; viewPre.querySelectorAll('.line-numbers-rows > span').forEach((span, i) => { if (span.dataset.lineWired) return; span.dataset.lineWired = '1'; const lineNum = i + 1; span.title = `Line ${lineNum}`; span.addEventListener('click', (e) => { if (e.shiftKey && _hlAnchor !== null) { const a = Math.min(_hlAnchor, lineNum); const b = Math.max(_hlAnchor, lineNum); _hlStart = a; _hlEnd = b; _drawHighlight(a, b); _updateLineUrl(a, b); } else if (_hlStart === lineNum && _hlEnd === lineNum) { // Clicking the sole highlighted line → clear _hlAnchor = null; _hlStart = null; _hlEnd = null; _clearHighlight(); _clearLineUrl(); } else { _hlAnchor = lineNum; _hlStart = lineNum; _hlEnd = lineNum; _drawHighlight(lineNum, lineNum); _updateLineUrl(lineNum, lineNum); } }); }); } function _clearHighlight() { const viewPre = document.getElementById('viewPre'); if (!viewPre) return; viewPre.querySelectorAll('.line-hl').forEach(el => el.remove()); viewPre.querySelectorAll('.line-numbers-rows > span').forEach( s => s.classList.remove('hl-active') ); } function _clearLineUrl() { const url = new URL(window.location.href); url.searchParams.delete('L'); history.replaceState(null, '', url.toString()); } function _drawHighlight(start, end) { const viewPre = document.getElementById('viewPre'); const code = document.getElementById('codeBlock'); if (!viewPre || !code) return; // Remove old bars and gutter classes viewPre.querySelectorAll('.line-hl').forEach(el => el.remove()); viewPre.querySelectorAll('.line-numbers-rows > span').forEach( (s, i) => s.classList.toggle('hl-active', i + 1 >= start && i + 1 <= end) ); const lineH = parseFloat(getComputedStyle(code).lineHeight) || 22.4; const padTop = parseFloat(getComputedStyle(viewPre).paddingTop) || 16; const frag = document.createDocumentFragment(); for (let i = start; i <= end; i++) { const bar = document.createElement('div'); bar.className = 'line-hl'; bar.style.top = (padTop + (i - 1) * lineH) + 'px'; bar.style.height = lineH + 'px'; frag.appendChild(bar); } // Insert before so bars sit below code text in z-order viewPre.insertBefore(frag, viewPre.firstChild); } function _scrollToLine(lineNum) { const code = document.getElementById('codeBlock'); const viewPre = document.getElementById('viewPre'); const scroller = document.querySelector('.view-full'); if (!code || !viewPre || !scroller) return; const lineH = parseFloat(getComputedStyle(code).lineHeight) || 22.4; const padTop = parseFloat(getComputedStyle(viewPre).paddingTop) || 16; scroller.scrollTop = Math.max(0, padTop + (lineNum - 1) * lineH - 80); } function _updateLineUrl(start, end) { const url = new URL(window.location.href); url.searchParams.set('L', start === end ? String(start) : `${start}-${end}`); history.replaceState(null, '', url.toString()); }