let pendingKey = null;
let pendingData = null;
const FONT_STACK_SANS = "'Vazirmatn Local', 'Vazirmatn', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
const FONT_STACK_MONO = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
const MAX_CUSTOM_EXPIRY_SECONDS = 31536000;
const MIN_CUSTOM_EXPIRY_SECONDS = 300;
function prefersInstantScroll() {
return window.matchMedia('(max-width: 640px)').matches;
}
function smartScrollIntoView(element) {
if (!element) return;
element.scrollIntoView({ behavior: prefersInstantScroll() ? 'auto' : 'smooth', block: 'start' });
}
function showError(msg) {
document.getElementById('errorDisplay').style.display = 'flex';
document.getElementById('errorMessage').textContent = msg;
}
function clearError() {
document.getElementById('errorDisplay').style.display = 'none';
}
function isRTL(text) {
const rtlRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
return rtlRegex.test(text || '');
}
function applyDirectionalStyles(element, text, rtlFont, ltrFont) {
if (!element) return;
if (isRTL(text)) {
element.style.direction = 'rtl';
element.style.fontFamily = rtlFont;
} else {
element.style.direction = 'ltr';
element.style.fontFamily = ltrFont;
}
}
function updateTextareaDirection(e) {
applyDirectionalStyles(e.target, e.target.value, FONT_STACK_SANS, FONT_STACK_MONO);
}
const contentTextarea = document.getElementById('content');
if (contentTextarea) {
contentTextarea.addEventListener('input', updateTextareaDirection);
}
const subjectInput = document.getElementById('subject');
if (subjectInput) {
subjectInput.addEventListener('input', function(e) {
applyDirectionalStyles(e.target, e.target.value, FONT_STACK_SANS, FONT_STACK_SANS);
});
}
const passwordInput = document.getElementById('passwordInput');
if (passwordInput) {
passwordInput.addEventListener('input', updatePasswordStrength);
}
const expiresInSelect = document.getElementById('expiresIn');
if (expiresInSelect) {
expiresInSelect.addEventListener('change', toggleCustomExpiry);
}
const editorToolbar = document.querySelector('.editor-toolbar');
if (editorToolbar && contentTextarea) {
editorToolbar.addEventListener('click', handleToolbarClick);
}
function handleToolbarClick(event) {
const button = event.target.closest('[data-action]');
if (!button || !contentTextarea) return;
const action = button.dataset.action;
applyToolbarAction(action);
}
function applyToolbarAction(action) {
switch (action) {
case 'bold':
wrapSelection('**', '**', 'bold text');
break;
case 'italic':
wrapSelection('*', '*', 'italic text');
break;
case 'strike':
wrapSelection('~~', '~~', 'strikethrough');
break;
case 'heading':
transformSelectedLines(function(line) {
return line ? `# ${line.replace(/^#{1,6}\s+/, '')}` : '# Heading';
}, { fallback: '# Heading' });
break;
case 'quote':
transformSelectedLines(function(line) {
return line ? `> ${line.replace(/^>\s?/, '')}` : '> Quote';
}, { fallback: '> Quote' });
break;
case 'bullet':
transformSelectedLines(function(line) {
return line ? `- ${line.replace(/^[-*+]\s+/, '')}` : '- List item';
}, { fallback: '- List item' });
break;
case 'numbered':
transformSelectedLines(function(line, index) {
return `${index + 1}. ${line.replace(/^\d+\.\s+/, '') || 'List item'}`;
}, { fallback: '1. List item' });
break;
case 'link': {
const selectedText = getSelectedText(contentTextarea) || 'link text';
const url = window.prompt('Enter the URL for this link:', 'https://');
if (url === null) {
focusEditor();
return;
}
const trimmedUrl = url.trim() || 'https://';
replaceSelection(`[${selectedText}](${trimmedUrl})`, { selectInserted: false });
break;
}
case 'code':
wrapSelection('`', '`', 'code');
break;
case 'codeblock':
wrapSelection('```\n', '\n```', 'your code here');
break;
default:
return;
}
updateTextareaDirection({ target: contentTextarea });
}
function getSelectedText(textarea) {
return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
}
function replaceSelection(replacement, options) {
if (!contentTextarea) return;
const settings = Object.assign({ selectInserted: true, selectionStartOffset: 0, selectionEndOffset: 0 }, options || {});
const start = contentTextarea.selectionStart;
const end = contentTextarea.selectionEnd;
const current = contentTextarea.value;
contentTextarea.value = current.slice(0, start) + replacement + current.slice(end);
if (settings.selectInserted) {
contentTextarea.setSelectionRange(start + settings.selectionStartOffset, start + replacement.length - settings.selectionEndOffset);
} else {
const caret = start + replacement.length;
contentTextarea.setSelectionRange(caret, caret);
}
focusEditor();
}
function wrapSelection(prefix, suffix, placeholder) {
if (!contentTextarea) return;
const selected = getSelectedText(contentTextarea);
const inner = selected || placeholder;
const replacement = `${prefix}${inner}${suffix}`;
const selectionStartOffset = prefix.length;
const selectionEndOffset = suffix.length;
replaceSelection(replacement, {
selectInserted: true,
selectionStartOffset,
selectionEndOffset
});
}
function transformSelectedLines(transformer, options) {
if (!contentTextarea) return;
const settings = Object.assign({ fallback: '' }, options || {});
const value = contentTextarea.value;
const start = contentTextarea.selectionStart;
const end = contentTextarea.selectionEnd;
const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
const nextNewlineIndex = value.indexOf('\n', end);
const lineEnd = nextNewlineIndex === -1 ? value.length : nextNewlineIndex;
const selectedBlock = value.slice(lineStart, lineEnd);
const lines = selectedBlock ? selectedBlock.split('\n') : [settings.fallback];
const transformed = lines.map(function(line, index) {
return transformer(line, index);
}).join('\n');
contentTextarea.value = value.slice(0, lineStart) + transformed + value.slice(lineEnd);
contentTextarea.setSelectionRange(lineStart, lineStart + transformed.length);
focusEditor();
}
function focusEditor() {
if (!contentTextarea) return;
contentTextarea.focus();
}
function togglePassword() {
const enabled = document.getElementById('enablePassword').checked;
const wrapper = document.getElementById('passwordWrapper');
wrapper.classList.toggle('show', enabled);
if (!enabled) {
document.getElementById('passwordInput').value = '';
resetPasswordStrength();
} else {
updatePasswordStrength();
}
}
function toggleCustomExpiry() {
const isCustom = document.getElementById('expiresIn').value === 'custom';
const wrapper = document.getElementById('customExpiryWrapper');
wrapper.classList.toggle('show', isCustom);
if (isCustom) {
setCustomExpiryBounds();
const input = document.getElementById('customExpiry');
if (!input.value) {
const defaultDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
input.value = toLocalDateTimeValue(defaultDate);
}
}
}
function setCustomExpiryBounds() {
const input = document.getElementById('customExpiry');
if (!input) return;
input.min = toLocalDateTimeValue(new Date(Date.now() + MIN_CUSTOM_EXPIRY_SECONDS * 1000));
input.max = toLocalDateTimeValue(new Date(Date.now() + MAX_CUSTOM_EXPIRY_SECONDS * 1000));
}
function toLocalDateTimeValue(date) {
const offset = date.getTimezoneOffset();
const local = new Date(date.getTime() - offset * 60000);
return local.toISOString().slice(0, 16);
}
const Base64 = {
encode(buf) {
const bytes = new Uint8Array(buf);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
},
decode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
const bin = atob(str);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
};
async function deriveKeyFromPassword(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
}
function genId() {
const arr = new Uint8Array(12);
crypto.getRandomValues(arr);
return Base64.encode(arr);
}
function buildEncryptedPayload(subject, content) {
return JSON.stringify({
subject: subject || '',
content: content
});
}
function parseDecryptedPayload(text) {
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
return {
subject: typeof parsed.subject === 'string' ? parsed.subject : '',
content: parsed.content
};
}
} catch (error) {
// Backward compatibility with old plain-text payloads.
}
return {
subject: '',
content: text
};
}
function resetCreateForm() {
document.getElementById('content').value = '';
document.getElementById('subject').value = '';
document.getElementById('passwordInput').value = '';
document.getElementById('enablePassword').checked = false;
document.getElementById('burnAfterRead').checked = false;
document.getElementById('expiresIn').value = '86400';
document.getElementById('customExpiry').value = '';
document.getElementById('customExpiryWrapper').classList.remove('show');
document.getElementById('passwordWrapper').classList.remove('show');
resetPasswordStrength();
applyDirectionalStyles(document.getElementById('content'), '', FONT_STACK_SANS, FONT_STACK_MONO);
applyDirectionalStyles(document.getElementById('subject'), '', FONT_STACK_SANS, FONT_STACK_SANS);
setCustomExpiryBounds();
}
function escapeHtml(text) {
return (text || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function escapeAttribute(text) {
return escapeHtml(text).replace(/`/g, '`');
}
function sanitizeUrl(url) {
const trimmed = (url || '').trim();
if (/^(https?:\/\/|mailto:)/i.test(trimmed)) {
return trimmed;
}
return null;
}
function renderInlineMarkdown(text) {
if (!text) return '';
const placeholders = [];
let work = text.replace(/\r\n/g, '\n');
work = work.replace(/`([^`\n]+)`/g, function(_, code) {
const token = `@@MDTOKEN${placeholders.length}@@`;
placeholders.push({ token, html: `${escapeHtml(code)}` });
return token;
});
work = work.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, function(_, label, url, title) {
const safeUrl = sanitizeUrl(url);
if (!safeUrl) {
return label;
}
const attrs = [`href="${escapeAttribute(safeUrl)}"`, 'target="_blank"', 'rel="noopener noreferrer"'];
if (title) {
attrs.push(`title="${escapeAttribute(title)}"`);
}
const token = `@@MDTOKEN${placeholders.length}@@`;
placeholders.push({ token, html: `${escapeHtml(label)}` });
return token;
});
let html = escapeHtml(work);
html = html.replace(/\*\*([^*]+)\*\*/g, '$1');
html = html.replace(/__([^_]+)__/g, '$1');
html = html.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1$2');
html = html.replace(/(^|[^_])_([^_\n]+)_(?!_)/g, '$1$2');
html = html.replace(/~~([^~]+)~~/g, '$1');
placeholders.forEach(function(entry) {
html = html.replace(entry.token, entry.html);
});
return html;
}
function renderMarkdown(content) {
const normalized = (content || '').replace(/\r\n/g, '\n');
const blocks = [];
const lines = normalized.split('\n');
let i = 0;
while (i < lines.length) {
if (!lines[i].trim()) {
i += 1;
continue;
}
if (lines[i].startsWith('```')) {
const lang = lines[i].slice(3).trim();
i += 1;
const codeLines = [];
while (i < lines.length && !lines[i].startsWith('```')) {
codeLines.push(lines[i]);
i += 1;
}
if (i < lines.length && lines[i].startsWith('```')) {
i += 1;
}
const langAttr = lang ? ` data-lang="${escapeAttribute(lang)}"` : '';
blocks.push(`
${escapeHtml(codeLines.join('\n'))}`);
continue;
}
if (/^#{1,6}\s+/.test(lines[i])) {
const match = lines[i].match(/^(#{1,6})\s+(.*)$/);
const level = match[1].length;
blocks.push(`${quoteLines.map(line => renderInlineMarkdown(line)).join('`); continue; } if (/^[-*+]\s+/.test(lines[i])) { const items = []; while (i < lines.length && /^[-*+]\s+/.test(lines[i])) { items.push(`
')}
${paragraphLines.map(line => renderInlineMarkdown(line)).join('
')}