更新数据截止日2026.01.08
This commit is contained in:
311
static/js/markdown-parser.js
Normal file
311
static/js/markdown-parser.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* 基础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;
|
||||
}
|
||||
Reference in New Issue
Block a user