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:
hypercross 2026-07-07 21:44:00 +08:00
parent 56af7dea42
commit 6b3a915350
2 changed files with 91 additions and 88 deletions

View File

@ -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");

View File

@ -1,4 +1,5 @@
import type { MarkedExtension, Tokens } from "marked"; import type { MarkedExtension, Tokens } from "marked";
import Slugger from "github-slugger";
/** /**
* CSV * CSV
@ -9,55 +10,75 @@ import type { MarkedExtension, Tokens } from "marked";
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 (
cell.includes(",") ||
cell.includes("\n") ||
cell.includes('"') ||
cell.includes("#")
) {
return `"${cell.replace(/"/g, '""')}"`; return `"${cell.replace(/"/g, '""')}"`;
} }
return cell; 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+/)) {
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; return true;
}else if(cell.text === 'md-remix-label'){ } else if (cell.text === "md-remix-label") {
roll = ' roll=true remix=true'; roll = " roll=true remix=true";
return true; return true;
} }
return cell.text === 'md-table-label' || cell.text === 'label'; return cell.text === "md-table-label" || cell.text === "label";
}); });
// 默认表格渲染 - 使用 marked 默认行为 // 默认表格渲染 - 使用 marked 默认行为
if (labelIndex === -1) return false; if (labelIndex === -1) return false;
const headers = token.header.map(cell => cell.text); const headers = token.header.map((cell) => cell.text);
headers[labelIndex] = 'label'; headers[labelIndex] = "label";
const rows = token.rows.map(row => row.map(cell => cell.text)); const rows = token.rows.map((row) => row.map((cell) => cell.text));
if(header.findIndex(header => header.text === 'body') < 0){ if (header.findIndex((header) => header.text === "body") < 0) {
// 收集所有非 label 列的表头 // 收集所有非 label 列的表头
const bodyColumns = headers const bodyColumns = headers.filter((cell) => cell !== "label");
.filter(cell => cell !== 'label');
// 构建 body 列的模板:**列名**{{列名}}\n\n // 构建 body 列的模板:**列名**{{列名}}\n\n
const bodyTemplate = bodyColumns const bodyTemplate = bodyColumns
.map(col => `**${col}**{{${col}}}`) .map((col) => `**${col}**{{${col}}}`)
.join('\n\n'); .join("\n\n");
headers.push('body'); headers.push("body");
rows.forEach(row => { rows.forEach((row) => {
row.push(bodyTemplate); row.push(bodyTemplate);
}); });
} }
@ -66,8 +87,8 @@ export default function markedTable(): MarkedExtension {
const csvData = tableToCSV(headers, rows); const csvData = tableToCSV(headers, rows);
// 渲染为 md-table 组件,内联 CSV 数据 // 渲染为 md-table 组件,内联 CSV 数据
return `<md-table ${roll}>${csvData}</md-table>\n`; return `<md-table ${roll}${spark}>${csvData}</md-table>\n`;
} },
} },
}; };
} }