astock-agent/frontend/src/lib/markdown.ts
2026-04-30 23:43:22 +08:00

121 lines
3.7 KiB
TypeScript

function escapeHtml(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatInline(value: string): string {
return escapeHtml(value)
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
}
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) => `<th>${formatInline(cell)}</th>`).join("");
const body = rows
.map((row) => `<tr>${row.map((cell) => `<td>${formatInline(cell)}</td>`).join("")}</tr>`)
.join("");
return `<div class="markdown-table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
function renderList(lines: string[]): string {
const items = lines.map((line) => line.replace(/^\s*[-*]\s+/, ""));
return `<ul>${items.map((item) => `<li>${formatInline(item)}</li>`).join("")}</ul>`;
}
function renderParagraph(lines: string[]): string {
return `<p>${lines.map((line) => formatInline(line.trim())).join("<br>")}</p>`;
}
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(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
if (heading) {
const level = heading[1].length;
blocks.push(`<h${level}>${formatInline(heading[2])}</h${level}>`);
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);
}