import { customElement, noShadowDOM } from "solid-element"; import { createSignal, For, Show, createEffect, createMemo, createResource, } from "solid-js"; import { TbOutlineDice5 } from "solid-icons/tb"; import { parseMarkdown } from "../markdown"; import { parseCSVString, CSV, processVariables } from "./utils/csv-loader"; import { resolveContentRef } from "./utils/resolve-content"; import { parseSparkTableCsv } from "./utils/spark-table"; import { areAllLabelsNumeric, weightedRandomIndex, } from "./utils/weighted-random";; export interface TableProps { roll?: boolean; remix?: boolean; } interface TableRow { label: string; body: string; [key: string]: string; } customElement( "md-table", { roll: false, remix: false }, (props, { element }) => { noShadowDOM(); const [rows, setRows] = createSignal>( [] as unknown as CSV, ); const [activeTab, setActiveTab] = createSignal(0); const [activeGroup, setActiveGroup] = createSignal(null); const [bodyHtml, setBodyHtml] = createSignal(""); let tabsContainer: HTMLDivElement | undefined; // 从 element 的 textContent 获取 CSV 路径或 inline CSV 数据 const rawContent = element?.textContent?.trim() || ""; // 隐藏原始文本内容 if (element) { element.textContent = ""; } // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径 const articleEl = element?.closest("article[data-src]"); const articlePath = articleEl?.getAttribute("data-src") || ""; // 解析引用:inline CSV 直接使用,否则通过 registry 解析(含内联内容 id) const [csvData] = createResource( () => ({ ref: rawContent, docPath: articlePath }), async ({ ref, docPath }) => { const content = await resolveContentRef(ref, docPath); if (content === null) { throw new Error(`Failed to resolve table content: "${ref}"`); } return content; }, ); // 当数据加载完成后更新 rows,并为火花表设置 data-spark(供 RevealManager // 悬停触发 /roll)。data-spark 在渲染时派生,不在扫描时注入。 createEffect(() => { const content = csvData(); if (!content) return; setRows(parseCSVString(content) as unknown as CSV); if (element) { const meta = parseSparkTableCsv(content); if (meta) { element.setAttribute("data-spark", meta.slug); } else { element.removeAttribute("data-spark"); } } }); // 检测是否有 group 列 const hasGroup = createMemo(() => { const allRows = rows(); return allRows.length > 0 && "group" in allRows[0]; }); // 获取所有分组 const groups = createMemo(() => { if (!hasGroup()) return []; const allRows = rows(); const groupSet = new Set(); for (const row of allRows) { if (row.group) { groupSet.add(row.group); } } return Array.from(groupSet).sort(); }); // 根据当前选中的分组过滤行 const filteredRows = createMemo(() => { const allRows = rows(); const group = activeGroup(); if (!group) return allRows; return allRows.filter((row) => row.group === group); }); // 处理 body 内容中的 {{prop}} 语法并解析 markdown const processBody = (body: string, currentRow: TableRow): string => { // 使用 parseMarkdown 统一入口(设置图标 base path 等) return parseMarkdown( processVariables(body, currentRow, rows(), filteredRows(), props.remix), articlePath, ); }; // 更新 body 内容 const updateBodyContent = () => { const filtered = filteredRows(); if (!csvData.loading && filtered.length > 0) { const index = Math.min(activeTab(), filtered.length - 1); const currentRow = filtered[index]; setBodyHtml(processBody(currentRow.body, currentRow)); } }; // 监听 activeTab 和 activeGroup 变化并更新内容 createEffect(() => { activeTab(); activeGroup(); updateBodyContent(); }); // 切换分组时重置 tab 索引 const handleGroupChange = (group: string | null) => { setActiveGroup(group); setActiveTab(0); }; // 随机切换 tab const handleRoll = () => { const filtered = filteredRows(); if (filtered.length === 0) return; // 检查所有 label 是否都是整数/整数范围格式 const labels = filtered.map((row) => row.label); if (areAllLabelsNumeric(labels)) { // 使用加权随机 const randomIndex = weightedRandomIndex(labels); if (randomIndex !== -1) { setActiveTab(randomIndex); } } else { // 使用简单随机 const randomIndex = Math.floor(Math.random() * filtered.length); setActiveTab(randomIndex); } // 滚动到可视区域 setTimeout(() => { const activeButton = tabsContainer?.querySelector(".border-blue-600"); activeButton?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest", }); }, 50); }; return (
{/* 分组 tabs */}
{(group) => ( )}
{/* 内容 tabs */}
{(row, index) => ( )}
0}>
); }, );