summaryrefslogtreecommitdiffstats
path: root/static/markdown.js
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-07-06 16:34:47 +0200
committerGab Virebent <gabriel1@virebent.art>2026-07-06 16:34:47 +0200
commit16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c (patch)
tree7ac7c321cacd62795cc23939bebdc9c48975377e /static/markdown.js
parent4fd6b4fefa7243f68fae7e24df3da46ee1a8e356 (diff)
downloadnymdrop-16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c.tar.gz
nymdrop-16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c.tar.xz
nymdrop-16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c.zip
Guard key material in RAM with memguard, add Markdown preview for message field
Reader now keeps the private key bytes and the per-submission ECDH/HKDF secrets in memguard.LockedBuffer (mlock + guaranteed wipe) instead of a manual zero loop. Added a regression test for the decrypt path. Message field gets a self-hosted Markdown toggle preview (no external library): headers, bold/italic, inline code, code blocks, blockquotes, lists, links. Output is HTML-escaped and link schemes are whitelisted before rendering. Submitted content is still the raw Markdown source, unchanged on the wire.
Diffstat (limited to 'static/markdown.js')
-rw-r--r--static/markdown.js149
1 files changed, 149 insertions, 0 deletions
diff --git a/static/markdown.js b/static/markdown.js
new file mode 100644
index 0000000..bad1a6d
--- /dev/null
+++ b/static/markdown.js
@@ -0,0 +1,149 @@
+// 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, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+}
+
+// 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, "<code>$1</code>");
+ out = out.replace(/\*\*([^*]+)\*\*|__([^_]+)__/g, (m, a, b) => `<strong>${a || b}</strong>`);
+ out = out.replace(/\*([^*]+)\*|_([^_]+)_/g, (m, a, b) => `<em>${a || b}</em>`);
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, url) =>
+ `<a href="${mdSafeHref(url)}" target="_blank" rel="noopener noreferrer">${label}</a>`);
+ 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("<p>" + para.map(mdInline).join("<br>") + "</p>");
+ 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("<pre><code>" + mdEscapeHtml(code.join("\n")) + "</code></pre>");
+ i++;
+ continue;
+ }
+
+ const heading = line.match(/^(#{1,3})\s+(.*)$/);
+ if (heading) {
+ flushPara();
+ const level = heading[1].length;
+ html.push(`<h${level}>${mdInline(heading[2])}</h${level}>`);
+ i++;
+ continue;
+ }
+
+ if (/^(---|\*\*\*)\s*$/.test(line)) {
+ flushPara();
+ html.push("<hr>");
+ 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("<blockquote>" + renderMarkdown(quote.join("\n")) + "</blockquote>");
+ continue;
+ }
+
+ if (/^[-*]\s+/.test(line)) {
+ flushPara();
+ const items = [];
+ while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
+ items.push(`<li>${mdInline(lines[i].replace(/^[-*]\s+/, ""))}</li>`);
+ i++;
+ }
+ html.push("<ul>" + items.join("") + "</ul>");
+ continue;
+ }
+
+ if (/^\d+\.\s+/.test(line)) {
+ flushPara();
+ const items = [];
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
+ items.push(`<li>${mdInline(lines[i].replace(/^\d+\.\s+/, ""))}</li>`);
+ i++;
+ }
+ html.push("<ol>" + items.join("") + "</ol>");
+ 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) ||
+ '<p class="md-empty">Nothing to preview yet.</p>';
+ textarea.hidden = true;
+ preview.hidden = false;
+ toggle.textContent = "Edit";
+ } else {
+ preview.hidden = true;
+ textarea.hidden = false;
+ toggle.textContent = "Preview";
+ }
+ });
+})();