function escapeHtml(value: string): string {
return value
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function formatInline(value: string): string {
return escapeHtml(value)
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1");
}
function isTableSeparator(line: string): boolean {
const cells = line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
return cells.length > 1 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
}
function splitTableRow(line: string): string[] {
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
}
function renderTable(lines: string[]): string {
const headers = splitTableRow(lines[0] ?? "");
const rows = lines.slice(2).map(splitTableRow);
const head = headers.map((cell) => `
${formatInline(cell)} | `).join("");
const body = rows
.map((row) => `${row.map((cell) => `| ${formatInline(cell)} | `).join("")}
`)
.join("");
return ``;
}
function renderList(lines: string[]): string {
const items = lines.map((line) => line.replace(/^\s*[-*]\s+/, ""));
return `${items.map((item) => `- ${formatInline(item)}
`).join("")}
`;
}
function renderParagraph(lines: string[]): string {
return `${lines.map((line) => formatInline(line.trim())).join("
")}
`;
}
export function markdownToHtml(md: string): string {
const lines = md.replace(/\r\n/g, "\n").split("\n");
const blocks: string[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index] ?? "";
const trimmed = line.trim();
if (!trimmed) {
index += 1;
continue;
}
if (trimmed.startsWith("```")) {
const codeLines: string[] = [];
index += 1;
while (index < lines.length && !(lines[index] ?? "").trim().startsWith("```")) {
codeLines.push(lines[index] ?? "");
index += 1;
}
index += 1;
blocks.push(`${escapeHtml(codeLines.join("\n"))}
`);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
if (heading) {
const level = heading[1].length;
blocks.push(`${formatInline(heading[2])}`);
index += 1;
continue;
}
if (trimmed.includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1] ?? "")) {
const tableLines = [line, lines[index + 1] ?? ""];
index += 2;
while (index < lines.length && (lines[index] ?? "").trim().includes("|")) {
tableLines.push(lines[index] ?? "");
index += 1;
}
blocks.push(renderTable(tableLines));
continue;
}
if (/^\s*[-*]\s+/.test(line)) {
const listLines = [line];
index += 1;
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index] ?? "")) {
listLines.push(lines[index] ?? "");
index += 1;
}
blocks.push(renderList(listLines));
continue;
}
const paragraphLines = [line];
index += 1;
while (
index < lines.length &&
(lines[index] ?? "").trim() &&
!/^(#{1,4})\s+/.test((lines[index] ?? "").trim()) &&
!/^\s*[-*]\s+/.test(lines[index] ?? "") &&
!((lines[index] ?? "").trim().includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1] ?? ""))
) {
paragraphLines.push(lines[index] ?? "");
index += 1;
}
blocks.push(renderParagraph(paragraphLines));
}
return blocks.join("");
}
export function formatMarkdown(text: string): string {
return markdownToHtml(text);
}