refactor(cli): Extract shared deck and frontmatter utils
Create deck-utils.ts and frontmatter/shared.ts to consolidate duplicated logic. Rename ensure-deck-preview.ts to preview-deck.ts.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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';
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { parse } from "csv-parse/browser/esm/sync";
|
||||
import { stringify } from "csv-stringify/browser/esm/sync";
|
||||
import type { DeckFrontmatter } from "../frontmatter/read-frontmatter.js";
|
||||
import {
|
||||
parseFrontMatter,
|
||||
serializeFrontMatter,
|
||||
} from "../frontmatter/shared.js";
|
||||
|
||||
/**
|
||||
* 卡牌数据
|
||||
@@ -23,7 +26,7 @@ export interface CardCrudParams {
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
action: 'create' | 'read' | 'update' | 'delete';
|
||||
action: "create" | "read" | "update" | "delete";
|
||||
/**
|
||||
* 卡牌数据(单张或数组)
|
||||
*/
|
||||
@@ -44,41 +47,6 @@ export interface CardCrudResult {
|
||||
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)
|
||||
*/
|
||||
@@ -87,19 +55,19 @@ function loadCSVWithFrontmatter(filePath: string): {
|
||||
records: CardData[];
|
||||
headers: string[];
|
||||
} {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const { frontmatter, csvContent } = parseFrontMatter(content);
|
||||
|
||||
const records = parse(csvContent, {
|
||||
columns: true,
|
||||
comment: '#',
|
||||
comment: "#",
|
||||
trim: true,
|
||||
skipEmptyLines: true
|
||||
skipEmptyLines: true,
|
||||
}) as CardData[];
|
||||
|
||||
// 获取表头
|
||||
const firstLine = csvContent.split('\n')[0];
|
||||
const headers = firstLine.split(',').map(h => h.trim());
|
||||
const firstLine = csvContent.split("\n")[0];
|
||||
const headers = firstLine.split(",").map((h) => h.trim());
|
||||
|
||||
return { frontmatter, records, headers };
|
||||
}
|
||||
@@ -111,30 +79,30 @@ function saveCSVWithFrontmatter(
|
||||
filePath: string,
|
||||
frontmatter: DeckFrontmatter | undefined,
|
||||
records: CardData[],
|
||||
headers?: string[]
|
||||
headers?: string[],
|
||||
): void {
|
||||
// 序列化 frontmatter
|
||||
const frontmatterStr = frontmatter ? serializeFrontMatter(frontmatter) : '';
|
||||
const frontmatterStr = frontmatter ? serializeFrontMatter(frontmatter) : "";
|
||||
|
||||
// 确定表头
|
||||
if (!headers || headers.length === 0) {
|
||||
// 从 records 和 frontmatter.fields 推断表头
|
||||
headers = ['label'];
|
||||
headers = ["label"];
|
||||
if (frontmatter?.fields && Array.isArray(frontmatter.fields)) {
|
||||
for (const field of frontmatter.fields) {
|
||||
if (field.name && typeof field.name === 'string') {
|
||||
if (field.name && typeof field.name === "string") {
|
||||
headers.push(field.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.push('body');
|
||||
headers.push("body");
|
||||
}
|
||||
|
||||
// 确保所有 record 都有 headers 中的列
|
||||
for (const record of records) {
|
||||
for (const header of headers) {
|
||||
if (!(header in record)) {
|
||||
record[header] = '';
|
||||
record[header] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,11 +110,11 @@ function saveCSVWithFrontmatter(
|
||||
// 序列化 CSV
|
||||
const csvContent = stringify(records, {
|
||||
header: true,
|
||||
columns: headers
|
||||
columns: headers,
|
||||
});
|
||||
|
||||
// 写入文件
|
||||
writeFileSync(filePath, frontmatterStr + csvContent, 'utf-8');
|
||||
writeFileSync(filePath, frontmatterStr + csvContent, "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,10 +135,10 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
const { csv_file, action, cards, label } = params;
|
||||
|
||||
// 检查文件是否存在(create 操作可以不存在)
|
||||
if (action !== 'create' && !existsSync(csv_file)) {
|
||||
if (action !== "create" && !existsSync(csv_file)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `文件不存在:${csv_file}`
|
||||
message: `文件不存在:${csv_file}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,8 +157,8 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
|
||||
// 执行操作
|
||||
switch (action) {
|
||||
case 'create': {
|
||||
const newCards = Array.isArray(cards) ? cards : (cards ? [cards] : []);
|
||||
case "create": {
|
||||
const newCards = Array.isArray(cards) ? cards : cards ? [cards] : [];
|
||||
for (const card of newCards) {
|
||||
if (!card.label) {
|
||||
card.label = generateNextLabel(records);
|
||||
@@ -202,16 +170,22 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
success: true,
|
||||
message: `成功创建 ${newCards.length} 张卡牌`,
|
||||
cards: newCards,
|
||||
count: newCards.length
|
||||
count: newCards.length,
|
||||
};
|
||||
}
|
||||
|
||||
case 'read': {
|
||||
const labelsToRead = Array.isArray(label) ? label : (label ? [label] : null);
|
||||
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 || ''));
|
||||
resultCards = records.filter((r) =>
|
||||
labelsToRead.includes(r.label || ""),
|
||||
);
|
||||
} else {
|
||||
resultCards = records;
|
||||
}
|
||||
@@ -220,20 +194,26 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
success: true,
|
||||
message: `成功读取 ${resultCards.length} 张卡牌`,
|
||||
cards: resultCards,
|
||||
count: resultCards.length
|
||||
count: resultCards.length,
|
||||
};
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
const labelsToUpdate = Array.isArray(label) ? label : (label ? [label] : null);
|
||||
const updateCards = Array.isArray(cards) ? cards : (cards ? [cards] : []);
|
||||
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);
|
||||
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++;
|
||||
@@ -243,7 +223,9 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
// 按 cards 中的 label 更新
|
||||
for (const updateCard of updateCards) {
|
||||
if (updateCard.label) {
|
||||
const index = records.findIndex(r => r.label === updateCard.label);
|
||||
const index = records.findIndex(
|
||||
(r) => r.label === updateCard.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
records[index] = { ...records[index], ...updateCard };
|
||||
updatedCount++;
|
||||
@@ -257,23 +239,29 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
success: true,
|
||||
message: `成功更新 ${updatedCount} 张卡牌`,
|
||||
cards: updateCards,
|
||||
count: updatedCount
|
||||
count: updatedCount,
|
||||
};
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const labelsToDelete = Array.isArray(label) ? label : (label ? [label] : null);
|
||||
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 || ''));
|
||||
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)
|
||||
records = records.filter(
|
||||
(r) => !cardsToDelete.some((c) => c.label && r.label === c.label),
|
||||
);
|
||||
deletedCount = beforeCount - records.length;
|
||||
}
|
||||
@@ -282,20 +270,20 @@ export function cardCrud(params: CardCrudParams): CardCrudResult {
|
||||
return {
|
||||
success: true,
|
||||
message: `成功删除 ${deletedCount} 张卡牌`,
|
||||
count: deletedCount
|
||||
count: deletedCount,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
message: `未知操作:${action}`
|
||||
message: `未知操作:${action}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `操作失败:${error instanceof Error ? error.message : '未知错误'}`
|
||||
message: `操作失败:${error instanceof Error ? error.message : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user