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

273 lines
8.8 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表格解析器
* 语法说明:
* ^n# - 向上合并n个单元格
* >n# - 向左合并n个单元格
* ^n>m# - 同时向上合并n个单元格,向左合并m个单元格
*/
class MergeTableParser {
constructor() {
this.patterns = {
mergeUp: /\^(\d+)#/, // ^2#
mergeLeft: />(\d+)#/, // >3#
mergeBoth: /\^(\d+)>(\d+)#/, // ^2>3#
};
}
/**
* 解析Markdown表格并转换为HTML表格
* @param {string} markdown - Markdown表格文本
* @returns {string} - HTML表格
*/
parseMarkdownTable(markdown) {
const lines = markdown.trim().split('\n');
const tableData = this.parseTableLines(lines);
return this.generateHTMLTable(tableData);
}
/**
* 解析表格行数据
* @param {string[]} lines - 表格行数组
* @returns {Object} - 表格数据对象
*/
parseTableLines(lines) {
const rows = [];
let headers = [];
let separatorIndex = -1;
// 解析表头和分隔符
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('|') && line.endsWith('|')) {
const cells = line.slice(1, -1).split('|').map(cell => cell.trim());
if (line.includes('---') || line.includes('===')) {
separatorIndex = i;
continue;
}
if (separatorIndex === -1) {
headers = cells;
} else {
rows.push(cells);
}
}
}
return { headers, rows, separatorIndex };
}
/**
* 生成HTML表格
* @param {Object} tableData - 表格数据
* @returns {string} - HTML表格字符串
*/
generateHTMLTable(tableData) {
const { headers, rows } = tableData;
const skipMap = new Map(); // 用于标记需要跳过的单元格
let html = '<table border="1" style="border-collapse: collapse; width: 100%;">\n';
// 生成表头(支持合并单元格)
if (headers.length > 0) {
html += ' <thead>\n <tr>\n';
// 处理表头合并单元格
headers.forEach((header, colIndex) => {
// 检查当前单元格是否应该被跳过
const key = `-1,${colIndex}`; // 表头行索引为-1
if (skipMap.has(key)) {
return;
}
const mergeInfo = this.parseMergeSyntax(header);
let cellHtml = '';
if (mergeInfo.rowspan > 1 || mergeInfo.colspan > 1) {
cellHtml = ` <th rowspan="${mergeInfo.rowspan}" colspan="${mergeInfo.colspan}">${this.escapeHTML(mergeInfo.content)}</th>\n`;
// 标记被合并的表头单元格
this.markMergedCells(-1, colIndex, mergeInfo, skipMap);
} else {
cellHtml = ` <th>${this.escapeHTML(header)}</th>\n`;
}
html += cellHtml;
});
html += ' </tr>\n </thead>\n';
}
// 生成表格主体
html += ' <tbody>\n';
// 第一遍:标记所有需要合并的单元格
rows.forEach((row, rowIndex) => {
row.forEach((cell, colIndex) => {
const mergeInfo = this.parseMergeSyntax(cell);
if (mergeInfo.rowspan > 1 || mergeInfo.colspan > 1) {
this.markMergedCells(rowIndex, colIndex, mergeInfo, skipMap);
}
});
});
// 第二遍:生成HTML,跳过被合并的单元格
rows.forEach((row, rowIndex) => {
html += ' <tr>\n';
let colOffset = 0; // 列偏移量,用于处理被跳过的单元格
row.forEach((cell, colIndex) => {
// 检查当前单元格是否应该被跳过
const key = `${rowIndex},${colIndex}`;
if (skipMap.has(key)) {
colOffset++;
return;
}
const mergeInfo = this.parseMergeSyntax(cell);
let cellHtml = '';
if (mergeInfo.rowspan > 1 || mergeInfo.colspan > 1) {
cellHtml = ` <td rowspan="${mergeInfo.rowspan}" colspan="${mergeInfo.colspan}">${this.escapeHTML(mergeInfo.content)}</td>\n`;
} else {
cellHtml = ` <td>${this.escapeHTML(cell)}</td>\n`;
}
html += cellHtml;
});
html += ' </tr>\n';
});
html += ' </tbody>\n</table>';
return html;
}
/**
* 解析合并语法
* @param {string} cell - 单元格内容
* @returns {Object} - 合并信息
*/
parseMergeSyntax(cell) {
const result = {
content: cell,
rowspan: 1,
colspan: 1,
shouldSkip: false
};
// 检查合并语法
const bothMatch = cell.match(this.patterns.mergeBoth);
const upMatch = cell.match(this.patterns.mergeUp);
const leftMatch = cell.match(this.patterns.mergeLeft);
if (bothMatch) {
result.rowspan = parseInt(bothMatch[1]) + 1;
result.colspan = parseInt(bothMatch[2]) + 1;
result.content = cell.replace(this.patterns.mergeBoth, '').trim();
} else if (upMatch) {
result.rowspan = parseInt(upMatch[1]) + 1;
result.content = cell.replace(this.patterns.mergeUp, '').trim();
} else if (leftMatch) {
result.colspan = parseInt(leftMatch[1]) + 1;
result.content = cell.replace(this.patterns.mergeLeft, '').trim();
}
return result;
}
/**
* 标记被合并的单元格
* @param {number} rowIndex - 行索引
* @param {number} colIndex - 列索引
* @param {Object} mergeInfo - 合并信息
* @param {Map} skipMap - 跳过映射
*/
markMergedCells(rowIndex, colIndex, mergeInfo, skipMap) {
for (let r = 0; r < mergeInfo.rowspan; r++) {
for (let c = 0; c < mergeInfo.colspan; c++) {
if (r === 0 && c === 0) continue; // 跳过主单元格
// 处理表头合并(行索引为-1)
const targetRow = rowIndex === -1 ? -1 : rowIndex + r;
const key = `${targetRow},${colIndex + c}`;
skipMap.set(key, true);
}
}
}
/**
* 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换行符
}
/**
* 将HTML表格转换回Markdown格式
* @param {string} html - HTML表格
* @returns {string} - Markdown表格
*/
htmlToMarkdown(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const table = doc.querySelector('table');
if (!table) return '';
let markdown = '';
const rows = table.querySelectorAll('tr');
rows.forEach((row, rowIndex) => {
const cells = row.querySelectorAll('th, td');
let rowMarkdown = '|';
cells.forEach(cell => {
const rowspan = parseInt(cell.getAttribute('rowspan') || '1');
const colspan = parseInt(cell.getAttribute('colspan') || '1');
let content = cell.textContent.trim();
// 添加合并语法
if (rowspan > 1) {
content += ` ^${rowspan - 1}`;
}
if (colspan > 1) {
content += ` >${colspan - 1}`;
}
rowMarkdown += ` ${content} |`;
});
markdown += rowMarkdown + '\n';
// 添加分隔符行(仅在第一行后)
if (rowIndex === 0) {
markdown += '|' + cells.map(() => '---').join('|') + '|\n';
}
});
return markdown;
}
}
// 导出为全局变量
if (typeof window !== 'undefined') {
window.MergeTableParser = MergeTableParser;
}
// 使用示例
if (typeof module !== 'undefined' && module.exports) {
module.exports = MergeTableParser;
}