refactor: simplify spark button injection logic
Update the spark button injection to use `data-spark` slugs on `md-table` elements instead of manual table parsing. This improves reliability by leveraging the slug generated during markdown rendering.
This commit is contained in:
parent
56af7dea42
commit
6b3a915350
|
|
@ -183,8 +183,6 @@ const LINK_SVG =
|
||||||
|
|
||||||
// ---- Spark button (tables) ----
|
// ---- Spark button (tables) ----
|
||||||
|
|
||||||
const DICE_HEADER_RE = /^d\d+$/i;
|
|
||||||
|
|
||||||
function injectSparkButtons(
|
function injectSparkButtons(
|
||||||
root: Element,
|
root: Element,
|
||||||
normalizedPath: string,
|
normalizedPath: string,
|
||||||
|
|
@ -196,40 +194,24 @@ function injectSparkButtons(
|
||||||
);
|
);
|
||||||
if (pageTables.length === 0) return;
|
if (pageTables.length === 0) return;
|
||||||
|
|
||||||
const tables = root.querySelectorAll("table");
|
// Spark tables are rendered as <md-table data-spark="columnSlug">
|
||||||
tables.forEach((table) => {
|
const sparkTables = root.querySelectorAll("md-table[data-spark]");
|
||||||
// Find the first header row — check thead first, then first tr
|
sparkTables.forEach((el) => {
|
||||||
const thead = table.querySelector("thead");
|
const colSlug = el.getAttribute("data-spark");
|
||||||
const firstRow =
|
if (!colSlug) return;
|
||||||
thead?.rows[0] ??
|
|
||||||
table.querySelector("tbody tr:first-child, tr:first-child");
|
|
||||||
if (!firstRow || !(firstRow instanceof HTMLTableRowElement)) return;
|
|
||||||
|
|
||||||
const firstCell = firstRow.cells[0];
|
// Find the matching completions entry by column slug
|
||||||
if (!firstCell) return;
|
const match = pageTables.find((st) => {
|
||||||
const firstHeader = firstCell.textContent?.trim() ?? "";
|
// st.slug is the combined slug (pageName-columnSlug);
|
||||||
if (!DICE_HEADER_RE.test(firstHeader)) return;
|
// colSlug is just the column part. Match by checking if
|
||||||
|
// the combined slug ends with the column slug.
|
||||||
// Collect data headers from the remaining cells
|
return st.slug.endsWith(`-${colSlug}`);
|
||||||
const dataHeaders: string[] = [];
|
});
|
||||||
for (let i = 1; i < firstRow.cells.length; i++) {
|
|
||||||
const text = firstRow.cells[i].textContent?.trim() ?? "";
|
|
||||||
if (text) dataHeaders.push(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match against completions: find the spark table with matching
|
|
||||||
// notation and data headers (order matters)
|
|
||||||
const match = pageTables.find(
|
|
||||||
(st) =>
|
|
||||||
st.notation.toLowerCase() === firstHeader.toLowerCase() &&
|
|
||||||
st.headers.length === dataHeaders.length &&
|
|
||||||
st.headers.every((h, i) => h === dataHeaders[i]),
|
|
||||||
);
|
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
// Inject the spark button
|
// Inject the spark button into the <md-table> wrapper
|
||||||
const btn = createSparkButton(match.slug);
|
const btn = createSparkButton(match.slug);
|
||||||
const wrapper = table.parentElement;
|
const wrapper = el.parentElement;
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
wrapper.style.position = "relative";
|
wrapper.style.position = "relative";
|
||||||
wrapper.classList.add("group");
|
wrapper.classList.add("group");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { MarkedExtension, Tokens } from "marked";
|
import type { MarkedExtension, Tokens } from "marked";
|
||||||
|
import Slugger from "github-slugger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将表格数据转换为 CSV 格式字符串
|
* 将表格数据转换为 CSV 格式字符串
|
||||||
|
|
@ -7,67 +8,87 @@ import type { MarkedExtension, Tokens } from "marked";
|
||||||
* @returns CSV 格式字符串
|
* @returns CSV 格式字符串
|
||||||
*/
|
*/
|
||||||
function tableToCSV(headers: string[], rows: string[][]): string {
|
function tableToCSV(headers: string[], rows: string[][]): string {
|
||||||
const escapeCell = (cell: string) => {
|
const escapeCell = (cell: string) => {
|
||||||
// 如果单元格包含逗号、换行或引号,需要转义
|
// 如果单元格包含逗号、换行或引号,需要转义
|
||||||
if (cell.includes(',') || cell.includes('\n') || cell.includes('"') || cell.includes("#")) {
|
if (
|
||||||
return `"${cell.replace(/"/g, '""')}"`;
|
cell.includes(",") ||
|
||||||
}
|
cell.includes("\n") ||
|
||||||
return cell;
|
cell.includes('"') ||
|
||||||
};
|
cell.includes("#")
|
||||||
|
) {
|
||||||
|
return `"${cell.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return cell;
|
||||||
|
};
|
||||||
|
|
||||||
const headerLine = headers.map(escapeCell).join(',');
|
const headerLine = headers.map(escapeCell).join(",");
|
||||||
const dataLines = rows.map(row => row.map(escapeCell).join(','));
|
const dataLines = rows.map((row) => row.map(escapeCell).join(","));
|
||||||
|
|
||||||
return [headerLine, ...dataLines].join('\n');
|
return [headerLine, ...dataLines].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Spark table: first column header is a pure dice formula (d6, d20, etc.) */
|
||||||
|
const SPARK_DICE_RE = /^d\d+$/i;
|
||||||
|
|
||||||
export default function markedTable(): MarkedExtension {
|
export default function markedTable(): MarkedExtension {
|
||||||
return {
|
return {
|
||||||
renderer: {
|
renderer: {
|
||||||
table(token: Tokens.Table) {
|
table(token: Tokens.Table) {
|
||||||
// 检查表头是否包含 md-table-label
|
// 检查表头是否包含 md-table-label
|
||||||
const header = token.header;
|
const header = token.header;
|
||||||
let roll = '';
|
let roll = "";
|
||||||
const labelIndex = header.findIndex(cell => {
|
let spark = "";
|
||||||
if(cell.text === 'md-roll-label' || cell.text.match(/(\d+)?d\d+/)){
|
const labelIndex = header.findIndex((cell) => {
|
||||||
roll = ' roll=true';
|
if (cell.text === "md-roll-label" || cell.text.match(/(\d+)?d\d+/)) {
|
||||||
return true;
|
roll = " roll=true";
|
||||||
}else if(cell.text === 'md-remix-label'){
|
// If this is a spark table (first column is a pure dice formula),
|
||||||
roll = ' roll=true remix=true';
|
// compute the data-column slug for DOM injection
|
||||||
return true;
|
if (SPARK_DICE_RE.test(cell.text)) {
|
||||||
}
|
const slugger = new Slugger();
|
||||||
return cell.text === 'md-table-label' || cell.text === 'label';
|
const dataHeaders = header
|
||||||
});
|
.filter((_, i) => i !== 0)
|
||||||
|
.map((h) => h.text);
|
||||||
// 默认表格渲染 - 使用 marked 默认行为
|
const slug = dataHeaders
|
||||||
if(labelIndex === -1) return false;
|
.map((h) => slugger.slug(h.toLowerCase()))
|
||||||
|
.join("-");
|
||||||
const headers = token.header.map(cell => cell.text);
|
spark = ` data-spark="${slug}"`;
|
||||||
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 数据
|
|
||||||
return `<md-table ${roll}>${csvData}</md-table>\n`;
|
|
||||||
}
|
}
|
||||||
|
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 数据
|
||||||
|
return `<md-table ${roll}${spark}>${csvData}</md-table>\n`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue