Add safe markdown renderer for operator-authored landing copy
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/* md.js — tiny, safe markdown subset for user-authored copy.
|
||||
* Supports: # headings, **bold**, *italic*, [links](url), - lists, blank-line paragraphs.
|
||||
* Everything is HTML-escaped FIRST, so authored text can never inject markup.
|
||||
*/
|
||||
const esc = (s) => String(s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
|
||||
export function renderMarkdown(src) {
|
||||
const lines = esc(src || '').split(/\r?\n/);
|
||||
const out = [];
|
||||
let para = [], list = null;
|
||||
|
||||
const flushPara = () => { if (para.length) { out.push(`<p>${inline(para.join(' '))}</p>`); para = []; } };
|
||||
const flushList = () => { if (list) { out.push(`<ul>${list.join('')}</ul>`); list = null; } };
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (!line) { flushPara(); flushList(); continue; }
|
||||
|
||||
const h = line.match(/^(#{1,3})\s+(.*)$/);
|
||||
if (h) { flushPara(); flushList(); const n = h[1].length + 1; out.push(`<h${n}>${inline(h[2])}</h${n}>`); continue; }
|
||||
|
||||
const li = line.match(/^[-*]\s+(.*)$/);
|
||||
if (li) { flushPara(); (list ||= []).push(`<li>${inline(li[1])}</li>`); continue; }
|
||||
|
||||
flushList();
|
||||
para.push(line);
|
||||
}
|
||||
flushPara(); flushList();
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
function inline(t) {
|
||||
return t
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
// links: only http(s) and site-relative, so no javascript: URLs
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+|\/[^\s)]*)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
}
|
||||
Reference in New Issue
Block a user