type ParseState = {
inCodeBlock: boolean;
codeLang: string;
paragraphBuffer: string[];
listType: "ul" | "ol" | null;
listBuffer: string[];
};
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function applyInlineMarkdown(value: string): string {
const escaped = escapeHtml(value);
return escaped
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/~~([^~]+)~~/g, "$1")
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1');
}
function flushParagraph(state: ParseState, html: string[]): void {
if (state.paragraphBuffer.length === 0) {
return;
}
html.push(`
${applyInlineMarkdown(state.paragraphBuffer.join(" "))}
`); state.paragraphBuffer = []; } function flushList(state: ParseState, html: string[]): void { if (!state.listType || state.listBuffer.length === 0) { return; } html.push(`<${state.listType}>${state.listBuffer.join("")}${state.listType}>`); state.listType = null; state.listBuffer = []; } export function markdownToHtml(markdown: string): string { const lines = markdown.replace(/\r\n/g, "\n").split("\n"); const html: string[] = []; const state: ParseState = { inCodeBlock: false, codeLang: "", paragraphBuffer: [], listType: null, listBuffer: [], }; for (const rawLine of lines) { const line = rawLine.trimEnd(); if (line.startsWith("```")) { flushParagraph(state, html); flushList(state, html); if (!state.inCodeBlock) { state.inCodeBlock = true; state.codeLang = line.slice(3).trim(); const className = state.codeLang ? ` class="language-${escapeHtml(state.codeLang)}"` : ""; html.push(``);
} else {
state.inCodeBlock = false;
state.codeLang = "";
html.push("");
}
continue;
}
if (state.inCodeBlock) {
html.push(`${escapeHtml(rawLine)}\n`);
continue;
}
if (line.length === 0) {
flushParagraph(state, html);
flushList(state, html);
continue;
}
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
flushParagraph(state, html);
flushList(state, html);
const level = headingMatch[1].length;
html.push(``); continue; } const ulMatch = line.match(/^[-*+]\s+(.+)$/); if (ulMatch) { flushParagraph(state, html); if (state.listType !== "ul") { flushList(state, html); state.listType = "ul"; } state.listBuffer.push(`${applyInlineMarkdown(blockquoteMatch[1])}