import { readFileSync, writeFileSync, existsSync } from 'fs'; import { parse } from 'csv-parse/browser/esm/sync'; import { stringify } from 'csv-stringify/browser/esm/sync'; import yaml from 'js-yaml'; import type { DeckFrontmatter } from '../frontmatter/read-frontmatter.js'; /** * 卡牌数据 */ export interface CardData { label?: string; [key: string]: string | undefined; } /** * 卡牌 CRUD 参数 */ export interface CardCrudParams { /** * CSV 文件路径 */ csv_file: string; /** * 操作类型 */ action: 'create' | 'read' | 'update' | 'delete'; /** * 卡牌数据(单张或数组) */ cards?: CardData | CardData[]; /** * 要读取/更新的卡牌 label(用于 read/update/delete) */ label?: string | string[]; } /** * 卡牌 CRUD 结果 */ export interface CardCrudResult { success: boolean; message: string; cards?: CardData[]; count?: number; } /** * 解析 CSV 文件的 frontmatter */ function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } { const parts = content.trim().split(/(?:^|\n)---\s*\n/g); if (parts.length !== 3 || parts[0] !== '') { return { csvContent: content }; } try { const frontmatterStr = parts[1].trim(); const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined; const csvContent = parts.slice(2).join('---\n').trimStart(); return { frontmatter, csvContent }; } catch (error) { console.warn('Failed to parse front matter:', error); return { csvContent: content }; } } /** * 序列化 frontmatter 为 YAML 字符串 */ function serializeFrontMatter(frontmatter: DeckFrontmatter): string { const yamlStr = yaml.dump(frontmatter, { indent: 2, lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false }); return `---\n${yamlStr}---\n`; } /** * 加载 CSV 数据(包含 frontmatter) */ function loadCSVWithFrontmatter(filePath: string): { frontmatter?: DeckFrontmatter; records: CardData[]; headers: string[]; } { const content = readFileSync(filePath, 'utf-8'); const { frontmatter, csvContent } = parseFrontMatter(content); const records = parse(csvContent, { columns: true, comment: '#', trim: true, skipEmptyLines: true }) as CardData[]; // 获取表头 const firstLine = csvContent.split('\n')[0]; const headers = firstLine.split(',').map(h => h.trim()); return { frontmatter, records, headers }; } /** * 保存 CSV 数据(包含 frontmatter) */ function saveCSVWithFrontmatter( filePath: string, frontmatter: DeckFrontmatter | undefined, records: CardData[], headers?: string[] ): void { // 序列化 frontmatter const frontmatterStr = frontmatter ? serializeFrontMatter(frontmatter) : ''; // 确定表头 if (!headers || headers.length === 0) { // 从 records 和 frontmatter.fields 推断表头 headers = ['label']; if (frontmatter?.fields && Array.isArray(frontmatter.fields)) { for (const field of frontmatter.fields) { if (field.name && typeof field.name === 'string') { headers.push(field.name); } } } headers.push('body'); } // 确保所有 record 都有 headers 中的列 for (const record of records) { for (const header of headers) { if (!(header in record)) { record[header] = ''; } } } // 序列化 CSV const csvContent = stringify(records, { header: true, columns: headers }); // 写入文件 writeFileSync(filePath, frontmatterStr + csvContent, 'utf-8'); } /** * 生成下一个 label */ function generateNextLabel(records: CardData[]): string { const maxLabel = records.reduce((max, record) => { const label = record.label ? parseInt(record.label, 10) : 0; return label > max ? label : max; }, 0); return (maxLabel + 1).toString(); } /** * 卡牌 CRUD 操作 */ export function cardCrud(params: CardCrudParams): CardCrudResult { const { csv_file, action, cards, label } = params; // 检查文件是否存在(create 操作可以不存在) if (action !== 'create' && !existsSync(csv_file)) { return { success: false, message: `文件不存在:${csv_file}` }; } try { let frontmatter: DeckFrontmatter | undefined; let records: CardData[] = []; let headers: string[] = []; // 加载现有数据 if (existsSync(csv_file)) { const data = loadCSVWithFrontmatter(csv_file); frontmatter = data.frontmatter; records = data.records; headers = data.headers; } // 执行操作 switch (action) { case 'create': { const newCards = Array.isArray(cards) ? cards : (cards ? [cards] : []); for (const card of newCards) { if (!card.label) { card.label = generateNextLabel(records); } records.push(card); } saveCSVWithFrontmatter(csv_file, frontmatter, records, headers); return { success: true, message: `成功创建 ${newCards.length} 张卡牌`, cards: newCards, count: newCards.length }; } case 'read': { const labelsToRead = Array.isArray(label) ? label : (label ? [label] : null); let resultCards: CardData[]; if (labelsToRead && labelsToRead.length > 0) { resultCards = records.filter(r => labelsToRead.includes(r.label || '')); } else { resultCards = records; } return { success: true, message: `成功读取 ${resultCards.length} 张卡牌`, cards: resultCards, count: resultCards.length }; } case 'update': { const labelsToUpdate = Array.isArray(label) ? label : (label ? [label] : null); const updateCards = Array.isArray(cards) ? cards : (cards ? [cards] : []); let updatedCount = 0; if (labelsToUpdate && labelsToUpdate.length > 0) { // 按 label 更新 for (const updateCard of updateCards) { const targetLabel = updateCard.label || labelsToUpdate[updatedCount % labelsToUpdate.length]; const index = records.findIndex(r => r.label === targetLabel); if (index !== -1) { records[index] = { ...records[index], ...updateCard }; updatedCount++; } } } else { // 按 cards 中的 label 更新 for (const updateCard of updateCards) { if (updateCard.label) { const index = records.findIndex(r => r.label === updateCard.label); if (index !== -1) { records[index] = { ...records[index], ...updateCard }; updatedCount++; } } } } saveCSVWithFrontmatter(csv_file, frontmatter, records, headers); return { success: true, message: `成功更新 ${updatedCount} 张卡牌`, cards: updateCards, count: updatedCount }; } case 'delete': { const labelsToDelete = Array.isArray(label) ? label : (label ? [label] : null); let deletedCount = 0; if (labelsToDelete && labelsToDelete.length > 0) { const beforeCount = records.length; records = records.filter(r => !labelsToDelete.includes(r.label || '')); deletedCount = beforeCount - records.length; } else if (cards) { const cardsToDelete = Array.isArray(cards) ? cards : [cards]; const beforeCount = records.length; records = records.filter(r => !cardsToDelete.some(c => c.label && r.label === c.label) ); deletedCount = beforeCount - records.length; } saveCSVWithFrontmatter(csv_file, frontmatter, records, headers); return { success: true, message: `成功删除 ${deletedCount} 张卡牌`, count: deletedCount }; } default: return { success: false, message: `未知操作:${action}` }; } } catch (error) { return { success: false, message: `操作失败:${error instanceof Error ? error.message : '未知错误'}` }; } }