import type { ReactNode } from "react";
export function renderPlainMarkdownText(text: string, keyPrefix: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /https?:\/\/[^\s)]+/g;
let cursor = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(text)) !== null) {
if (match.index > cursor) {
nodes.push(text.slice(cursor, match.index));
}
const url = match[0];
nodes.push(
{url}
,
);
cursor = match.index + url.length;
}
if (cursor < text.length) {
nodes.push(text.slice(cursor));
}
return nodes.length > 0 ? nodes : [text];
}
export function renderMarkdownInline(text: string, keyPrefix: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /(`[^`\n]+`|\*\*[\s\S]+?\*\*|__[\s\S]+?__|~~[\s\S]+?~~|\[[^\]]+\]\(https?:\/\/[^)\s]+\)|https?:\/\/[^\s)]+)/g;
let cursor = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(text)) !== null) {
const token = match[0];
if (match.index > cursor) {
nodes.push(...renderPlainMarkdownText(text.slice(cursor, match.index), `${keyPrefix}-text-${cursor}`));
}
if (token.startsWith("`") && token.endsWith("`")) {
nodes.push({token.slice(1, -1)});
} else if ((token.startsWith("**") && token.endsWith("**")) || (token.startsWith("__") && token.endsWith("__"))) {
nodes.push({renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-strong-${match.index}`)});
} else if (token.startsWith("~~") && token.endsWith("~~")) {
nodes.push({renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-del-${match.index}`)});
} else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) {
const labelEnd = token.indexOf("](");
const label = token.slice(1, labelEnd);
const href = token.slice(labelEnd + 2, -1);
nodes.push(
{renderMarkdownInline(label, `${keyPrefix}-md-link-label-${match.index}`)}
,
);
} else {
nodes.push(
{token}
,
);
}
cursor = match.index + token.length;
}
if (cursor < text.length) {
nodes.push(...renderPlainMarkdownText(text.slice(cursor), `${keyPrefix}-text-${cursor}`));
}
return nodes.length > 0 ? nodes : [text];
}
export function splitMarkdownTableRow(line: string): string[] {
return line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim());
}
export function isMarkdownTableDivider(line: string): boolean {
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}
export function isMarkdownBlockStart(line: string, nextLine?: string): boolean {
const trimmed = line.trim();
return (
trimmed === "" ||
/^```/.test(trimmed) ||
/^#{1,4}\s+/.test(trimmed) ||
/^>\s?/.test(trimmed) ||
/^[-*+]\s+/.test(trimmed) ||
/^\d+[.)]\s+/.test(trimmed) ||
/^[-*_]{3,}$/.test(trimmed) ||
(line.includes("|") && !!nextLine && isMarkdownTableDivider(nextLine))
);
}
export function renderMarkdownBlocks(text: string): ReactNode[] {
const lines = text.replace(/\r\n?/g, "\n").trim().split("\n");
const blocks: ReactNode[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index] ?? "";
const trimmed = line.trim();
if (!trimmed) {
index += 1;
continue;
}
const fenceMatch = trimmed.match(/^```(\S+)?/);
if (fenceMatch) {
const language = fenceMatch[1] || "";
const codeLines: string[] = [];
index += 1;
while (index < lines.length && !lines[index].trim().startsWith("```")) {
codeLines.push(lines[index]);
index += 1;
}
if (index < lines.length) index += 1;
blocks.push(
{codeLines.join("\n")}
| {renderMarkdownInline(header, `table-head-${cellIndex}`)} | ))}
|---|
| {renderMarkdownInline(row[cellIndex] || "", `table-cell-${rowIndex}-${cellIndex}`)} | ))}
{quoteLines.map((quoteLine, quoteIndex) => (, ); continue; } const unorderedMatch = trimmed.match(/^[-*+]\s+(.+)$/); const orderedMatch = trimmed.match(/^\d+[.)]\s+(.+)$/); if (unorderedMatch || orderedMatch) { const ordered = Boolean(orderedMatch); const ListTag = ordered ? "ol" : "ul"; const items: string[] = []; while (index < lines.length) { const itemMatch = ordered ? lines[index].trim().match(/^\d+[.)]\s+(.+)$/) : lines[index].trim().match(/^[-*+]\s+(.+)$/); if (!itemMatch) break; items.push(itemMatch[1]); index += 1; } blocks.push({renderMarkdownInline(quoteLine, `quote-${index}-${quoteIndex}`)}
))}
{renderMarkdownInline(paragraphText, `paragraph-${index}`)}
); } return blocks; }