1
16
17const MAX_INPUT = 10_000;
18
19function escapeHtml(s: string): string {
20 return s
21 .replace(/&/g, "&")
22 .replace(/</g, "<")
23 .replace(/>/g, ">")
24 .replace(/"/g, """)
25 .replace(/'/g, "'");
26}
27
28
29function safeUrl(raw: string): string | null {
30 const trimmed = raw.trim();
31 if (!/^(https?:\/\/)/i.test(trimmed)) return null;
32
33
34 if (/[\u0000-\u001f]/.test(trimmed)) return null;
35 return trimmed;
36}
37
38
39function inline(s: string): string {
40 let out = s;
41
42 out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
43 out = out.replace(/__([^_]+)__/g, "<strong>$1</strong>");
44
45 out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
46 out = out.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
47
48 out = out.replace(/\[([^\]]+)\]\(([^()\s]+)\)/g, (_m, label: string, url: string) => {
49 const safe = safeUrl(url);
50 if (!safe) return `${label}(${url})`;
51 return `<a href="${safe}" rel="noopener noreferrer" target="_blank">${label}</a>`;
52 });
53 return out;
54}
55
56
60export function renderIntroMarkdown(md: string): string {
61 if (typeof md !== "string" || md.length === 0) return "";
62 const source = escapeHtml(md.slice(0, MAX_INPUT));
63 const lines = source.split(/\r?\n/);
64 const blocks: string[] = [];
65 let i = 0;
66
67 while (i < lines.length) {
68 const line = lines[i];
69 if (line.trim() === "") {
70 i += 1;
71 continue;
72 }
73 const ul = /^\s*[-*]\s+/.test(line);
74 const ol = /^\s*\d+[.)]\s+/.test(line);
75 if (ul || ol) {
76 const items: string[] = [];
77 const re = ul ? /^\s*[-*]\s+(.*)$/ : /^\s*\d+[.)]\s+(.*)$/;
78 while (i < lines.length) {
79 const m = lines[i].match(re);
80 if (!m) break;
81 items.push(`<li>${inline(m[1])}</li>`);
82 i += 1;
83 }
84 const tag = ul ? "ul" : "ol";
85 blocks.push(`<${tag}>${items.join("")}</${tag}>`);
86 continue;
87 }
88
89 const para: string[] = [];
90 while (i < lines.length) {
91 const l = lines[i];
92 if (l.trim() === "" || /^\s*[-*]\s+/.test(l) || /^\s*\d+[.)]\s+/.test(l)) break;
93 para.push(inline(l));
94 i += 1;
95 }
96 if (para.length > 0) blocks.push(`<p>${para.join("<br>")}</p>`);
97 }
98
99 return blocks.join("");
100}
101