diff --git a/src/components/md-table.tsx b/src/components/md-table.tsx index daf590e..3f1c230 100644 --- a/src/components/md-table.tsx +++ b/src/components/md-table.tsx @@ -2,7 +2,7 @@ import { customElement, noShadowDOM } from 'solid-element'; import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js'; import { marked } from '../markdown'; import { resolvePath } from './utils/path'; -import { loadCSV } from './utils/csv-loader'; +import {loadCSV, CSV, processVariables} from './utils/csv-loader'; export interface TableProps { roll?: boolean; @@ -17,7 +17,7 @@ interface TableRow { customElement('md-table', { roll: false, remix: false }, (props, { element }) => { noShadowDOM(); - const [rows, setRows] = createSignal([]); + const [rows, setRows] = createSignal>([]); const [activeTab, setActiveTab] = createSignal(0); const [activeGroup, setActiveGroup] = createSignal(null); const [bodyHtml, setBodyHtml] = createSignal(''); @@ -78,21 +78,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) => // 处理 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; + return marked.parse(processVariables(body, currentRow, rows(), filteredRows(), props.remix)) as string; }; // 更新 body 内容 diff --git a/src/components/utils/csv-loader.ts b/src/components/utils/csv-loader.ts index b288d27..2a74488 100644 --- a/src/components/utils/csv-loader.ts +++ b/src/components/utils/csv-loader.ts @@ -1,22 +1,61 @@ import { parse } from 'csv-parse/browser/esm/sync'; +import yaml from 'js-yaml'; /** * 全局缓存已加载的 CSV 内容 */ const csvCache = new Map[]>(); +/** + * 解析 front matter + * @param content 包含 front matter 的内容 + * @returns 解析结果,包含 front matter 和剩余内容 + */ +function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainingContent: string } { + // 检查是否以 --- 开头 + if (!content.trim().startsWith('---')) { + return { remainingContent: content }; + } + + // 分割内容 + const parts = content.split(/(?:^|\n)---\s*\n/); + + // 至少需要三个部分:空字符串、front matter、剩余内容 + if (parts.length < 3) { + return { remainingContent: content }; + } + + try { + // 解析 YAML front matter + const frontmatterStr = parts[1].trim(); + const frontmatter = yaml.load(frontmatterStr) as JSONObject; + + // 剩余内容是第三部分及之后的所有内容 + const remainingContent = parts.slice(2).join('---\n').trimStart(); + + return { frontmatter, remainingContent }; + } catch (error) { + console.warn('Failed to parse front matter:', error); + return { remainingContent: content }; + } +} + /** * 加载 CSV 文件 * @template T 返回数据的类型,默认为 Record */ -export async function loadCSV>(path: string): Promise { +export async function loadCSV>(path: string): Promise> { if (csvCache.has(path)) { - return csvCache.get(path)! as T[]; + return csvCache.get(path)! as CSV; } const response = await fetch(path); const content = await response.text(); - const records = parse(content, { + + // 解析 front matter + const { frontmatter, remainingContent } = parseFrontMatter(content); + + const records = parse(remainingContent, { columns: true, comment: '#', trim: true, @@ -24,6 +63,36 @@ export async function loadCSV>(path: string): Promise }); const result = records as Record[]; + // 添加 front matter 到结果中 + const csvResult = result as CSV; + if (frontmatter) { + csvResult.frontmatter = frontmatter; + } + csvCache.set(path, result); - return result as T[]; + return csvResult; } + +type JSONData = JSONArray | JSONObject | string | number | boolean | null; +interface JSONArray extends Array {} +interface JSONObject extends Record {} + +export type CSV = T[] & { + frontmatter?: JSONObject; +} + +export function processVariables (body: string, currentRow: T, csv: CSV, filtered?: T[], remix?: boolean): string { + const rolled = filtered || csv; + + function replaceProp(key: string) { + const row = remix ? + rolled[Math.floor(Math.random() * rolled.length)] : + currentRow; + const frontMatter = csv.frontmatter; + if(key in row) return row[key]; + if(frontMatter && key in frontMatter) return frontMatter[key]; + return ''; + } + + return body.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`); +} \ No newline at end of file