// 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("
") + "
" + mdEscapeHtml(code.join("\n")) + "");
i++;
continue;
}
const heading = line.match(/^(#{1,3})\s+(.*)$/);
if (heading) {
flushPara();
const level = heading[1].length;
html.push(`" + renderMarkdown(quote.join("\n")) + ""); continue; } if (/^[-*]\s+/.test(line)) { flushPara(); const items = []; while (i < lines.length && /^[-*]\s+/.test(lines[i])) { items.push(`
Nothing to preview yet.
'; textarea.hidden = true; preview.hidden = false; toggle.textContent = "Edit"; } else { preview.hidden = true; textarea.hidden = false; toggle.textContent = "Preview"; } }); })();