// NymDrop — minimal self-hosted Markdown preview for the message field. // No external library: a small, auditable subset is enough for a // submission form, and it avoids pulling a third-party JS dependency into // a page whose whole point is minimizing trust in client-side code. // // The rendered HTML never leaves the browser — the encrypted submission // still carries the raw Markdown source text untouched (see app.js). This // is purely a "what will this look like" aid for the person writing it. function mdEscapeHtml(str) { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Only allow link schemes that can't execute script in the rendered preview. function mdSafeHref(url) { if (/^(https?:|mailto:)/i.test(url)) return mdEscapeHtml(url); return "#"; } function mdInline(text) { let out = mdEscapeHtml(text); out = out.replace(/`([^`]+)`/g, "$1"); out = out.replace(/\*\*([^*]+)\*\*|__([^_]+)__/g, (m, a, b) => `${a || b}`); out = out.replace(/\*([^*]+)\*|_([^_]+)_/g, (m, a, b) => `${a || b}`); out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, url) => `${label}`); return out; } function renderMarkdown(source) { const lines = source.replace(/\r\n/g, "\n").split("\n"); const html = []; let i = 0; let para = []; function flushPara() { if (para.length) { html.push("

" + para.map(mdInline).join("
") + "

"); para = []; } } while (i < lines.length) { const line = lines[i]; if (/^```/.test(line)) { flushPara(); const code = []; i++; while (i < lines.length && !/^```/.test(lines[i])) { code.push(lines[i]); i++; } html.push("
" + mdEscapeHtml(code.join("\n")) + "
"); i++; continue; } const heading = line.match(/^(#{1,3})\s+(.*)$/); if (heading) { flushPara(); const level = heading[1].length; html.push(`${mdInline(heading[2])}`); i++; continue; } if (/^(---|\*\*\*)\s*$/.test(line)) { flushPara(); html.push("
"); i++; continue; } if (/^>\s?/.test(line)) { flushPara(); const quote = []; while (i < lines.length && /^>\s?/.test(lines[i])) { quote.push(lines[i].replace(/^>\s?/, "")); i++; } html.push("
" + renderMarkdown(quote.join("\n")) + "
"); continue; } if (/^[-*]\s+/.test(line)) { flushPara(); const items = []; while (i < lines.length && /^[-*]\s+/.test(lines[i])) { items.push(`
  • ${mdInline(lines[i].replace(/^[-*]\s+/, ""))}
  • `); i++; } html.push(""); continue; } if (/^\d+\.\s+/.test(line)) { flushPara(); const items = []; while (i < lines.length && /^\d+\.\s+/.test(lines[i])) { items.push(`
  • ${mdInline(lines[i].replace(/^\d+\.\s+/, ""))}
  • `); i++; } html.push("
      " + items.join("") + "
    "); continue; } if (line.trim() === "") { flushPara(); i++; continue; } para.push(line); i++; } flushPara(); return html.join("\n"); } (function() { const textarea = document.getElementById("message"); const preview = document.getElementById("message-preview"); const toggle = document.getElementById("preview-toggle"); if (!textarea || !preview || !toggle) return; let showingPreview = false; toggle.addEventListener("click", function() { showingPreview = !showingPreview; if (showingPreview) { preview.innerHTML = renderMarkdown(textarea.value) || '

    Nothing to preview yet.

    '; textarea.hidden = true; preview.hidden = false; toggle.textContent = "Edit"; } else { preview.hidden = true; textarea.hidden = false; toggle.textContent = "Preview"; } }); })();