ttrpg-tools/src/cli/tools/card/card-crud.ts

302 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 : '未知错误'}`
};
}
}