import { customElement, noShadowDOM } from 'solid-element'; import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js'; import { parse } from 'csv-parse/browser/esm/sync'; import { marked } from '../markdown'; interface TableRow { label: string; body: string; group?: string; [key: string]: string; } // 全局缓存已加载的 CSV 内容 const csvCache = new Map(); customElement('md-table', { roll: false, remix: false }, (props, { element }) => { noShadowDOM(); const [rows, setRows] = createSignal([]); const [activeTab, setActiveTab] = createSignal(0); const [activeGroup, setActiveGroup] = createSignal(null); const [bodyHtml, setBodyHtml] = createSignal(''); // 从 element 的 textContent 获取 CSV 路径 const src = element?.textContent?.trim() || ''; // 隐藏原始文本内容 if (element) { element.textContent = ''; } // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径 const articleEl = element?.closest('article[data-src]'); const articlePath = articleEl?.getAttribute('data-src') || ''; // 解析相对路径(相对于 markdown 文件所在目录) const resolvePath = (base: string, relative: string): string => { if (relative.startsWith('/')) { return relative; } const baseDir = base.substring(0, base.lastIndexOf('/') + 1); return baseDir + relative; }; const resolvedSrc = resolvePath(articlePath, src); // 加载 CSV 文件的函数 const loadCSV = async (path: string): Promise => { // 先从缓存获取 if (csvCache.has(path)) { return csvCache.get(path)!; } const response = await fetch(path); const content = await response.text(); const records = parse(content, { columns: true, comment: '#', trim: true, skipEmptyLines: true }); const result = records as TableRow[]; // 缓存结果 csvCache.set(path, result); return result; }; // 使用 createResource 加载 CSV,自动响应路径变化并避免重复加载 const [csvData] = createResource(() => resolvedSrc, loadCSV); // 当数据加载完成后更新 rows createEffect(() => { const data = csvData(); if (data) { setRows(data); } }); // 检测是否有 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 => { let processedBody = body; if (!props.remix) { // 不启用 remix 时,只替换当前行的引用 processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || ''); } else { // 启用 remix 时,每次引用使用随机行的内容 processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => { const randomRow = rows()[Math.floor(Math.random() * rows().length)]; return randomRow?.[key] || ''; }); } // 使用 marked 解析 markdown return marked.parse(processedBody) as string; }; // 更新 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 randomIndex = Math.floor(Math.random() * filteredRows().length); setActiveTab(randomIndex); }; return (
{/* 分组 tabs */}
{(group) => ( )}
{/* 内容 tabs */}
{(row, index) => ( )}
0}>
); });