Files
SmartChat/static/js/markdown-parser.js
2026-01-08 01:06:27 +08:00

311 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 基础Markdown解析器
* 支持标题、粗体、斜体、列表、链接、图片、代码、引用等基础语法
*/
class MarkdownParser {
constructor() {
this.patterns = {
heading: /^(#{1,6})\s+(.+)$/gm,
bold: /\*\*(.*?)\*\*/g,
italic: /\*(.*?)\*/g,
inlineCode: /`(.*?)`/g,
image: /!\[(.*?)]\((.*?)\)/g,
base64Image: /!\[(.*?)]\((data:image\/[^;]+;base64,[^\)]+)\)/gi,
imageLink: /\[(.*?)]\((.*?\.(?:jpg|jpeg|png|gif|bmp|webp|svg))\s*(?:"(.*?)")?\)/gi,
link: /\[(.*?)]\((?!.*\.(?:jpg|jpeg|png|gif|bmp|webp|svg))(.*?)\)/g,
blockquote: /^>\s+(.+)$/gm,
unorderedList: /^\s*[-*+]\s+(.+)$/gm,
orderedList: /^\d+\.\s+(.+)$/gm,
codeBlock: /```.*?\n([\s\S]*?)```/g,
horizontalRule: /^---$/gm,
comment: /<!--([\s\S]*?)-->/g
};
}
/**
* 解析Markdown文本为HTML
* @param {string} markdown - Markdown文本
* @returns {string} - HTML文本
*/
parse(markdown) {
let html = markdown;
// 处理代码块(先处理,避免其他语法干扰)
html = html.replace(this.patterns.codeBlock, (match, code) => {
// 保留代码内部的换行符,只去除首尾空白
let codeLines = code.split('\n');
let result = '<pre>';
for (let i in codeLines) {
if (codeLines[i].trim() === '') {
continue;
} else {
result += `<code>${codeLines[i]}</code><br>`;
}
}
result += '</pre>';
return result;
});
// 处理块引用
html = html.replace(this.patterns.blockquote, (match, text) => {
return `<blockquote>${this.parseInline(text)}</blockquote>`;
});
// 处理水平分割线
html = html.replace(this.patterns.horizontalRule, '<hr>');
// 处理注释(保留HTML注释)
html = html.replace(this.patterns.comment, '<!--$1-->');
// 处理列表(先处理有序列表,再处理无序列表,避免冲突)
html = this.processLists(html);
// 处理段落(将连续文本包装为段落)
html = this.wrapParagraphs(html);
return html;
}
/**
* 解析行内元素
* @param {string} text - 文本内容
* @returns {string} - 解析后的HTML
*/
parseInline(text) {
let html = text;
// 处理粗体
html = html.replace(this.patterns.bold, '<strong>$1</strong>');
// 处理斜体
html = html.replace(this.patterns.italic, '<em>$1</em>');
// 处理行内代码
html = html.replace(this.patterns.inlineCode, '<code>$1</code>');
// 处理普通图片格式 ![alt](url)
html = html.replace(this.patterns.image, '<img width="100%" src="$2" alt="$1">');
// 处理base64图片格式 ![alt](data:image/...)
html = html.replace(this.patterns.base64Image, '<img width="100%" src="$2" alt="$1">');
// 处理图片链接格式 [alt](image.jpg "title")
html = html.replace(this.patterns.imageLink, (match, alt, src, title) => {
const titleAttr = title ? ` title="${title}"` : '';
return `<img width="100%" src="${src}" alt="${alt}"${titleAttr}>`;
});
// 处理链接
html = html.replace(this.patterns.link, '<a href="$2">$1</a>');
return html;
}
/**
* 处理所有列表,先识别列表类型再处理列表项
* @param {string} html - HTML文本
* @returns {string} - 处理后的HTML
*/
processLists(html) {
// 先识别并标记列表类型
const lines = html.split('\n');
let result = '';
let currentListType = '';
let listItems = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.match(/^\d+\.\s/)) {
// 有序列表项
if (currentListType !== 'ol') {
// 结束前一个列表
if (listItems.length > 0) {
result += this.wrapListItems(listItems, currentListType);
listItems = [];
}
currentListType = 'ol';
}
const content = line.replace(/^\d+\.\s+/, '');
listItems.push(`<li>${this.parseInline(content)}</li>`);
} else if (line.match(/^[-*+]\s/)) {
// 无序列表项
if (currentListType !== 'ul') {
// 结束前一个列表
if (listItems.length > 0) {
result += this.wrapListItems(listItems, currentListType);
listItems = [];
}
currentListType = 'ul';
}
const content = line.replace(/^[-*+]\s+/, '');
listItems.push(`<li>${this.parseInline(content)}</li>`);
} else {
// 非列表项
if (listItems.length > 0) {
result += this.wrapListItems(listItems, currentListType);
listItems = [];
currentListType = '';
}
result += lines[i] + '\n';
}
}
// 处理最后一个列表
if (listItems.length > 0) {
result += this.wrapListItems(listItems, currentListType);
}
return result;
}
/**
* 包装列表项
* @param {Array} listItems - 列表项数组
* @param {string} listType - 列表类型(ul或ol)
* @returns {string} - 包装后的HTML列表
*/
wrapListItems(listItems, listType) {
if (listItems.length === 0) return '';
const listTag = listType === 'ol' ? 'ol' : 'ul';
return `<${listTag}>\n${listItems.join('\n')}\n</${listTag}>\n`;
}
/**
* 包装段落,保留Markdown原有换行
* @param {string} html - HTML文本
* @returns {string} - 包装后的HTML
*/
wrapParagraphs(html) {
const lines = html.split('\n');
let result = '';
let currentParagraph = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (!trimmedLine) {
// 空行,结束当前段落
if (currentParagraph) {
// 检查当前段落是否已经是块级元素
if (!currentParagraph.match(/^<(h[1-6]|blockquote|ul|ol|li|pre|hr)/)) {
result += `<p>${this.parseInline(currentParagraph)}</p>`;
} else {
result += currentParagraph;
}
currentParagraph = '';
}
result += '\n';
} else if (trimmedLine.match(/^<(h[1-6]|blockquote|ul|ol|li|pre|hr)/)) {
// 已经是块级元素,直接添加
if (currentParagraph) {
if (!currentParagraph.match(/^<(h[1-6]|blockquote|ul|ol|li|pre|hr)/)) {
result += `<p>${this.parseInline(currentParagraph)}</p>`;
} else {
result += currentParagraph;
}
currentParagraph = '';
}
result += line + '\n';
} else {
// 保留原有换行,不合并为一行
if (currentParagraph) {
currentParagraph += '\n' + line;
} else {
currentParagraph = line;
}
}
}
// 处理最后一个段落
if (currentParagraph) {
if (!currentParagraph.match(/^<(h[1-6]|blockquote|ul|ol|li|pre|hr)/)) {
result += `<p>${this.parseInline(currentParagraph)}</p>`;
} else {
result += currentParagraph;
}
}
return result;
}
/**
* HTML转义,支持保留<br>标签
* @param {string} text - 原始文本
* @returns {string} - 转义后的文本
*/
escapeHTML(text) {
// 首先转义所有HTML特殊字符
const escapedText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// 然后恢复<br>标签的功能
return escapedText
.replace(/&lt;br\s*\/?&gt;/gi, '<br>')
.replace(/&lt;br\s*\/?&gt;/gi, '<br>')
.replace(/\\n/g, '<br>'); // 也支持\n换行符
}
/**
* 专门解析表格(与MergeTableParser配合使用)
* @param {string} markdown - Markdown表格文本
* @returns {string} - HTML表格
*/
parseTable(markdown) {
// 使用MergeTableParser处理表格
if (typeof MergeTableParser !== 'undefined') {
const tableParser = new MergeTableParser();
return tableParser.parseMarkdownTable(markdown);
}
return markdown;
}
/**
* 完整解析Markdown,包含表格处理
* @param {string} markdown - 完整Markdown文本
* @returns {string} - 完整HTML
*/
parseFull(markdown) {
let html = markdown;
// 先处理标题,避免表格干扰
html = html.replace(this.patterns.heading, (match, hashes, text) => {
const level = hashes.length;
return `<h${level}>${this.parseInline(text)}</h${level}>`;
});
// 再提取并处理表格
const tableRegex = /(\|.*?\|\s*\n)+/g;
html = html.replace(tableRegex, (match) => {
// 检查是否是有效的表格格式
const lines = match.trim().split('\n');
if (lines.length >= 2 && lines.every(line => line.trim().startsWith('|') && line.trim().endsWith('|'))) {
return this.parseTable(match);
}
return match;
});
// 然后处理其他Markdown语法
return this.parse(html);
}
}
// 导出为全局变量
if (typeof window !== 'undefined') {
window.MarkdownParser = MarkdownParser;
}
// 使用示例
if (typeof module !== 'undefined' && module.exports) {
module.exports = MarkdownParser;
}