refactor: remove implicit content sniffing and table conversion

- Split loadCSV into parseCSVString (content) and loadCSVFromPath
  (path); drop isCSV/looksLikeCsv heuristics
- Delete coerceSparkTables and markedTable label-header magic;
  plain markdown tables now render as plain tables
- Add explicit markdown role=spark-table fence syntax that converts
  pipe tables to CSV at scan time, with dice-header validation
- Map ESM-only github-slugger and csv-parse browser build to CJS in
  jest config; add content-registry tests
This commit is contained in:
2026-09-08 21:33:12 +08:00
parent 5061c4d19d
commit 256c685f6c
13 changed files with 216 additions and 253 deletions
-2
View File
@@ -2,7 +2,6 @@ import { Marked, type MarkedExtension } from "marked";
import { createDirectives, presetDirectiveConfigs } from "marked-directive";
import markedAlert from "marked-alert";
import markedMermaid from "./mermaid";
import markedTable from "./table";
import { gfmHeadingId } from "marked-gfm-heading-id";
import markedColumns from "./columns";
import markedCodeBlockYamlTag from "./code-block-yaml-tag";
@@ -14,7 +13,6 @@ const marked = new Marked()
.use(gfmHeadingId())
.use(markedAlert())
.use(markedMermaid())
.use(markedTable())
.use(markedCodeBlockYamlTag())
.use(
createDirectives([
-84
View File
@@ -1,84 +0,0 @@
import type { MarkedExtension, Tokens } from "marked";
/**
* 将表格数据转换为 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;
};
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) {
const header = token.header;
let roll = "";
let remix = "";
// Spark tables (dice-formula first column) are handled upstream by
// `coerceSparkTables` in the content registry — they're rewritten to
// `:md-table` directives before rendering. This renderer only handles
// label-based tables (md-table-label / md-roll-label / md-remix-label).
const labelIndex = header.findIndex((cell) => {
if (cell.text === "md-roll-label") {
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";
});
// 默认表格渲染 - 使用 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 列的表头
const bodyColumns = headers.filter((cell) => cell !== "label");
// 构建 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 数据
// data-spark attribute is injected by the CLI directive scanner,
// not computed here.
return `<md-table ${roll}${remix}>${csvData}</md-table>\n`;
},
},
};
}