311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
/**
|
||
* 基础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>');
|
||
|
||
// 处理普通图片格式 
|
||
html = html.replace(this.patterns.image, '<img width="100%" src="$2" alt="$1">');
|
||
|
||
// 处理base64图片格式 
|
||
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
// 然后恢复<br>标签的功能
|
||
return escapedText
|
||
.replace(/<br\s*\/?>/gi, '<br>')
|
||
.replace(/<br\s*\/?>/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;
|
||
} |