ttrpg-tools/src/markdown/table.ts

95 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-03-21 18:30:40 +08:00
import type { MarkedExtension, Tokens } from "marked";
import Slugger from "github-slugger";
2026-03-21 18:30:40 +08:00
/**
* CSV
* @param headers
* @param rows
* @returns CSV
*/
function tableToCSV(headers: string[], rows: string[][]): string {
const escapeCell = (cell: string) => {
// 如果单元格包含逗号、换行或引号,需要转义
if (
cell.includes(",") ||
cell.includes("\n") ||
cell.includes('"') ||
cell.includes("#")
) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
};
2026-03-21 18:30:40 +08:00
const headerLine = headers.map(escapeCell).join(",");
const dataLines = rows.map((row) => row.map(escapeCell).join(","));
2026-03-21 18:30:40 +08:00
return [headerLine, ...dataLines].join("\n");
2026-03-21 18:30:40 +08:00
}
/** Spark table: first column header is a pure dice formula (d6, d20, etc.) */
const SPARK_DICE_RE = /^d\d+$/i;
2026-03-21 18:30:40 +08:00
export default function markedTable(): MarkedExtension {
return {
renderer: {
table(token: Tokens.Table) {
// 检查表头是否包含 md-table-label
const header = token.header;
let roll = "";
let spark = "";
const labelIndex = header.findIndex((cell) => {
if (cell.text === "md-roll-label" || cell.text.match(/(\d+)?d\d+/)) {
roll = " roll=true";
// If this is a spark table (first column is a pure dice formula),
// compute the data-column slug for DOM injection
if (SPARK_DICE_RE.test(cell.text)) {
const slugger = new Slugger();
const dataHeaders = header
.filter((_, i) => i !== 0)
.map((h) => h.text);
const slug = dataHeaders
.map((h) => slugger.slug(h.toLowerCase()))
.join("-");
spark = ` data-spark="${slug}"`;
}
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;
2026-03-21 18:30:40 +08:00
const headers = token.header.map((cell) => cell.text);
headers[labelIndex] = "label";
const rows = token.rows.map((row) => row.map((cell) => cell.text));
2026-03-21 18:30:40 +08:00
if (header.findIndex((header) => header.text === "body") < 0) {
// 收集所有非 label 列的表头
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);
});
2026-03-21 18:30:40 +08:00
}
// 生成 CSV 数据
const csvData = tableToCSV(headers, rows);
// 渲染为 md-table 组件,内联 CSV 数据
return `<md-table ${roll}${spark}>${csvData}</md-table>\n`;
},
},
};
2026-03-21 18:30:40 +08:00
}