121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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);
|
|
}
|