273 lines
8.8 KiB
JavaScript
273 lines
8.8 KiB
JavaScript
/**
|
||
* 支持合并单元格的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, '&')
|
||
.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换行符
|
||
}
|
||
|
||
/**
|
||
* 将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;
|
||
} |