1
13
14function esc(s: string): string {
15 return s
16 .replace(/&/g, "&")
17 .replace(/</g, "<")
18 .replace(/>/g, ">")
19 .replace(/"/g, """);
20}
21
22
23function safeHref(href: string): string {
24 const h = href.trim();
25 return /^https?:\/\
26}
27
28
31export function renderMarkdown(md: string): string {
32 const src = md.replace(/\r\n/g, "\n");
33 const lines = src.split("\n");
34 const out: string[] = [];
35 let inUl = false;
36 let inOl = false;
37 let para: string[] = [];
38
39 const closeLists = () => {
40 if (inUl) out.push("</ul>");
41 if (inOl) out.push("</ol>");
42 inUl = inOl = false;
43 };
44 const flushPara = () => {
45 if (para.length) {
46 out.push(`<p>${para.map(inline).join("<br>")}</p>`);
47 para = [];
48 }
49 };
50
51 const inline = (s: string): string => {
52 let t = esc(s);
53
54 t = t.replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`);
55 // bold then italic
56 t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
57 t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
58 // links
59 t = t.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text, href) => `<a href="${esc(safeHref(String(href)))}" rel="noopener noreferrer">${text}</a>`);
60 return t;
61 };
62
63 for (const raw of lines) {
64 const line = raw;
65 const h = /^(#{1,3})\s+(.*)$/.exec(line);
66 if (h) {
67 flushPara();
68 closeLists();
69 const level = h[1]!.length;
70 out.push(`<h${level}>${inline(h[2]!)}</h${level}>`);
71 continue;
72 }
73 const ul = /^\s*[-*]\s+(.*)$/.exec(line);
74 if (ul) {
75 flushPara();
76 if (inOl) closeLists();
77 if (!inUl) {
78 out.push("<ul>");
79 inUl = true;
80 }
81 out.push(`<li>${inline(ul[1]!)}</li>`);
82 continue;
83 }
84 const ol = /^\s*\d+\.\s+(.*)$/.exec(line);
85 if (ol) {
86 flushPara();
87 if (inUl) closeLists();
88 if (!inOl) {
89 out.push("<ol>");
90 inOl = true;
91 }
92 out.push(`<li>${inline(ol[1]!)}</li>`);
93 continue;
94 }
95 if (line.trim() === "") {
96 flushPara();
97 closeLists();
98 continue;
99 }
100 para.push(line);
101 }
102 flushPara();
103 closeLists();
104 return out.join("\n");
105}
106