import katex from 'katex';
export interface RenderMarkdownOptions {
preserveNewlines?: boolean;
}
/**
* 健壮的行内 Markdown、LaTeX 公式与有序/无序列表渲染引擎
* 支持:
* - 块级数学公式 ($$...$$) 与行内数学公式 ($...$)
* - 粗体 (**text** / __text__)
* - 斜体 (*text* / _text_)
* - 粗斜体 (***text***)
* - 删除线 (~~text~~)
* - 下划线 (text)
* - 行内代码 (`code`)
* - 超链接 ([text](url))
* - 有序列表 (1. item, 2. item ...)
* - 无序列表 (- item, * item ...)
* - 荧光高亮 ( / 预置语法)
* - 安全内联样式标签 ()
*/
export function renderInlineMarkdown(content: string, options: RenderMarkdownOptions = {}): string {
if (!content) return '';
let text = content;
// 1. 占位保护预处理:优先解析并提取数学公式,避免公式内部的 * 或 _ 被 Markdown 解析器误伤
// 注意:占位符严禁使用包含 _ 或 * 等 Markdown 特殊字符,采用纯大写字母标识符
const mathPlaceholders: string[] = [];
const placeholderPrefix = 'XMATHTOKENX';
// 1.1 块级独立公式 $$ ... $$
text = text.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
try {
const rendered = katex.renderToString(math.trim(), {
displayMode: true,
throwOnError: false,
});
const idx = mathPlaceholders.length;
mathPlaceholders.push(rendered);
return `${placeholderPrefix}${idx}XENDX`;
} catch {
const idx = mathPlaceholders.length;
mathPlaceholders.push(`$$${math}$$`);
return `${placeholderPrefix}${idx}XENDX`;
}
});
// 1.2 行内公式 $ ... $
text = text.replace(/\$([^\$\n]+?)\$/g, (_, math) => {
try {
const rendered = katex.renderToString(math.trim(), {
displayMode: false,
throwOnError: false,
});
const idx = mathPlaceholders.length;
mathPlaceholders.push(rendered);
return `${placeholderPrefix}${idx}XENDX`;
} catch {
const idx = mathPlaceholders.length;
mathPlaceholders.push(`$${math}$`);
return `${placeholderPrefix}${idx}XENDX`;
}
});
// 2. 行内代码保护 `code`
const codePlaceholders: string[] = [];
const codePlaceholderPrefix = 'XCODETOKENX';
text = text.replace(/`([^`\n]+)`/g, (_, code) => {
const rendered = `${escapeHtml(code)}`;
const idx = codePlaceholders.length;
codePlaceholders.push(rendered);
return `${codePlaceholderPrefix}${idx}XENDX`;
});
// 3. 粗斜体 ***text***
text = text.replace(/\*\*\*([^\*\n]+?)\*\*\*/g, '$1');
// 4. 粗体 **text** 或 __text__
text = text.replace(/\*\*([^\*\n]+?)\*\*/g, '$1');
text = text.replace(/__([^_\n]+?)__/g, '$1');
// 5. 斜体 *text* 或 _text_
text = text.replace(/\*([^\*\n]+?)\*/g, '$1');
text = text.replace(/_([^_\n]+?)_/g, '$1');
// 6. 删除线 ~~text~~
text = text.replace(/~~([^~\n]+?)~~/g, '$1');
// 7. 链接 [text](url)
text = text.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, '$1');
// 8. 还原行内代码占位符
codePlaceholders.forEach((codeHtml, idx) => {
text = text.replace(`${codePlaceholderPrefix}${idx}XENDX`, () => codeHtml);
});
// 9. 还原数学公式占位符
mathPlaceholders.forEach((mathHtml, idx) => {
text = text.replace(`${placeholderPrefix}${idx}XENDX`, () => mathHtml);
});
// 10. 处理有序列表、无序列表与换行
if (options.preserveNewlines !== false) {
const rawLines = text.split('\n');
const resultParts: string[] = [];
let i = 0;
while (i < rawLines.length) {
const line = rawLines[i];
const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
const unorderedMatch = line.match(/^(\s*)[-*+]\s+(.*)$/);
if (orderedMatch) {
const listItems: string[] = [];
const startNum = parseInt(orderedMatch[2], 10) || 1;
while (i < rawLines.length) {
const curMatch = rawLines[i].match(/^\s*\d+\.\s+(.*)$/);
if (!curMatch) break;
listItems.push(curMatch[1]);
i++;
}
resultParts.push(
`` +
listItems.map((li) => `- ${li}
`).join('') +
`
`
);
} else if (unorderedMatch) {
const listItems: string[] = [];
while (i < rawLines.length) {
const curMatch = rawLines[i].match(/^\s*[-*+]\s+(.*)$/);
if (!curMatch) break;
listItems.push(curMatch[1]);
i++;
}
resultParts.push(
`` +
listItems.map((li) => `- ${li}
`).join('') +
`
`
);
} else {
resultParts.push(line);
i++;
}
}
let outHtml = '';
for (let p = 0; p < resultParts.length; p++) {
const cur = resultParts[p];
if (p > 0) {
const prev = resultParts[p - 1];
const isPrevBlock = prev.startsWith('
';
}
}
outHtml += cur;
}
text = outHtml;
}
return text;
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}