website/src/lib/md.ts
2026-09-08 19:29:17 -04:00

75 lines
1.8 KiB
TypeScript

const esc = (s: string) =>
s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
function inline(s: string): string {
return esc(s)
.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`)
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/(^|[\s(])_([^_]+)_/g, "$1<em>$2</em>")
.replace(
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener">$1</a>',
);
}
export function renderMarkdown(src: string): string {
const lines = src.replace(/\r\n/g, "\n").split("\n");
const out: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (line.trim() === "") {
i++;
continue;
}
if (line.trim().startsWith("```")) {
const buf: string[] = [];
i++;
while (i < lines.length && !lines[i].trim().startsWith("```")) {
buf.push(esc(lines[i]));
i++;
}
i++;
out.push(`<pre><code>${buf.join("\n")}</code></pre>`);
continue;
}
const h = line.match(/^(#{2,4})\s+(.*)$/);
if (h) {
const level = h[1].length;
out.push(`<h${level}>${inline(h[2])}</h${level}>`);
i++;
continue;
}
if (/^\s*[-*]\s+/.test(line)) {
const items: string[] = [];
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
items.push(`<li>${inline(lines[i].replace(/^\s*[-*]\s+/, ""))}</li>`);
i++;
}
out.push(`<ul>${items.join("")}</ul>`);
continue;
}
const para: string[] = [];
while (
i < lines.length &&
lines[i].trim() !== "" &&
!lines[i].trim().startsWith("```") &&
!/^(#{2,4})\s+/.test(lines[i]) &&
!/^\s*[-*]\s+/.test(lines[i])
) {
para.push(lines[i]);
i++;
}
out.push(`<p>${inline(para.join(" "))}</p>`);
}
return out.join("\n");
}