ttrpg-tools/src/markdown/table.ts

74 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-03-21 18:30:40 +08:00
import type { MarkedExtension, Tokens } from "marked";
/**
* CSV
* @param headers
* @param rows
* @returns CSV
*/
function tableToCSV(headers: string[], rows: string[][]): string {
const escapeCell = (cell: string) => {
// 如果单元格包含逗号、换行或引号,需要转义
2026-03-22 00:27:59 +08:00
if (cell.includes(',') || cell.includes('\n') || cell.includes('"') || cell.includes("#")) {
2026-03-21 18:30:40 +08:00
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
};
const headerLine = headers.map(escapeCell).join(',');
const dataLines = rows.map(row => row.map(escapeCell).join(','));
return [headerLine, ...dataLines].join('\n');
}
export default function markedTable(): MarkedExtension {
return {
renderer: {
table(token: Tokens.Table) {
// 检查表头是否包含 md-table-label
const header = token.header;
let roll = '';
const labelIndex = header.findIndex(cell => {
if(cell.text === 'md-roll-label' || cell.text.match(/(\d+)?d\d+/)){
roll = ' roll=true';
return true;
}else if(cell.text === 'md-remix-label'){
roll = ' roll=true remix=true';
return true;
}
return cell.text === 'md-table-label' || cell.text === 'label';
});
2026-03-21 18:30:40 +08:00
// 默认表格渲染 - 使用 marked 默认行为
if(labelIndex === -1) return false;
const headers = token.header.map(cell => cell.text);
headers[labelIndex] = 'label';
const rows = token.rows.map(row => row.map(cell => cell.text));
if(header.findIndex(header => header.text === 'body') < 0){
// 收集所有非 label 列的表头
2026-03-21 19:09:54 +08:00
const bodyColumns = headers
.filter(cell => cell !== 'label');
2026-03-21 18:30:40 +08:00
// 构建 body 列的模板:**列名**{{列名}}\n\n
const bodyTemplate = bodyColumns
.map(col => `**${col}**{{${col}}}`)
.join('\n\n');
headers.push('body');
rows.forEach(row => {
row.push(bodyTemplate);
});
}
// 生成 CSV 数据
const csvData = tableToCSV(headers, rows);
// 渲染为 md-table 组件,内联 CSV 数据
return `<md-table ${roll}>${csvData}</md-table>\n`;
2026-03-21 18:30:40 +08:00
}
}
};
}