bastebin/static/js/paste_view.js

183 lines
6.8 KiB
JavaScript

'use strict';
/**
* paste_view.js — Logic for the paste view page.
* Depends on: crypto.js (PasteCrypto), app.js (copyToClipboard)
*
* Reads the site name from <meta name="site-name"> 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();
initLineNumbers();
});
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');
}
localStorage.setItem('show_line_numbers', checked);
// Re-highlight if a language is selected to force Prism to update the numbers span
const code = document.getElementById('codeBlock');
if (code && (code.className.includes('language-') || viewPre.className.includes('language-'))) {
// Prism's line-numbers plugin needs to clean up if turning off
if (!checked) {
const existing = viewPre.querySelector('.line-numbers-rows');
if (existing) existing.remove();
}
Prism.highlightElement(code);
}
};
toggle.addEventListener('change', updateLines);
// Initial state
if (isEnabled) {
viewPre.classList.add('line-numbers');
}
}
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() {
if (!_decryptedPaste) return;
const blob = new Blob([_decryptedPaste.content], { type: 'text/plain; charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'),
{ href: url, target: '_blank', rel: 'noopener noreferrer' });
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
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);
}