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:
hyper
2026-07-09 14:31:08 +08:00
parent 06c3916a95
commit a20063c624
9 changed files with 409 additions and 689 deletions
+12 -32
View File
@@ -1,5 +1,5 @@
import { readFileSync, existsSync } from 'fs';
import yaml from 'js-yaml';
import { readFileSync, existsSync } from "fs";
import { parseFrontMatter } from "./shared.js";
/**
* 读取 CSV frontmatter 的参数
@@ -52,7 +52,7 @@ export interface CardFieldStyle {
/**
* 朝向:上侧朝向 "n" | "w" | "s" | "e"(北/西/南/东)
*/
up?: 'n' | 'w' | 's' | 'e';
up?: "n" | "w" | "s" | "e";
}
/**
@@ -74,7 +74,7 @@ export interface DeckConfig {
grid?: string;
bleed?: number;
padding?: number;
shape?: 'rectangle' | 'circle' | 'hex' | 'diamond';
shape?: "rectangle" | "circle" | "hex" | "diamond";
layers?: string;
back_layers?: string;
[key: string]: unknown;
@@ -89,45 +89,25 @@ export interface ReadFrontmatterResult {
message: string;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
// 至少需要三个部分:空字符串、front matter、CSV 内容
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 };
}
}
/**
* 读取 CSV 文件的 frontmatter
*/
export function readFrontmatter(params: ReadFrontmatterParams): ReadFrontmatterResult {
export function readFrontmatter(
params: ReadFrontmatterParams,
): ReadFrontmatterResult {
const { csv_file } = params;
// 检查文件是否存在
if (!existsSync(csv_file)) {
return {
success: false,
message: `文件不存在:${csv_file}`
message: `文件不存在:${csv_file}`,
};
}
try {
// 读取文件内容
const content = readFileSync(csv_file, 'utf-8');
const content = readFileSync(csv_file, "utf-8");
// 解析 frontmatter
const { frontmatter } = parseFrontMatter(content);
@@ -136,19 +116,19 @@ export function readFrontmatter(params: ReadFrontmatterParams): ReadFrontmatterR
return {
success: true,
frontmatter: {},
message: `文件 ${csv_file} 没有 frontmatter,返回空对象`
message: `文件 ${csv_file} 没有 frontmatter,返回空对象`,
};
}
return {
success: true,
frontmatter,
message: `成功读取 ${csv_file} 的 frontmatter`
message: `成功读取 ${csv_file} 的 frontmatter`,
};
} catch (error) {
return {
success: false,
message: `读取失败:${error instanceof Error ? error.message : '未知错误'}`
message: `读取失败:${error instanceof Error ? error.message : "未知错误"}`,
};
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Shared frontmatter utilities — used by read-frontmatter, write-frontmatter,
* card-crud, preview-deck, and generate-card-deck.
*/
import yaml from "js-yaml";
import type { DeckFrontmatter } from "./read-frontmatter.js";
/**
* Parse YAML frontmatter from a CSV file's raw content.
* Expects `---\n...\n---\n` delimited frontmatter at the top of the file.
*/
export 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 };
}
}
/**
* Serialize a DeckFrontmatter object to YAML frontmatter string.
*/
export function serializeFrontMatter(frontmatter: DeckFrontmatter): string {
const yamlStr = yaml.dump(frontmatter, {
indent: 2,
lineWidth: -1,
noRefs: true,
quotingType: '"',
forceQuotes: false,
});
return `---\n${yamlStr}---\n`;
}
+20 -53
View File
@@ -1,6 +1,6 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import yaml from 'js-yaml';
import type { DeckFrontmatter } from './read-frontmatter.js';
import { readFileSync, writeFileSync, existsSync } from "fs";
import type { DeckFrontmatter } from "./read-frontmatter.js";
import { parseFrontMatter, serializeFrontMatter } from "./shared.js";
/**
* 写入 CSV frontmatter 的参数
@@ -29,70 +29,37 @@ export interface WriteFrontmatterResult {
csv_file?: string;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
// 至少需要三个部分:空字符串、front matter、CSV 内容
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
*/
export function writeFrontmatter(params: WriteFrontmatterParams): WriteFrontmatterResult {
export function writeFrontmatter(
params: WriteFrontmatterParams,
): WriteFrontmatterResult {
const { csv_file, frontmatter, merge = true } = params;
let csvContent = '';
let csvContent = "";
let existingFrontmatter: DeckFrontmatter | undefined;
// 如果文件存在且需要合并,先读取现有内容
if (merge && existsSync(csv_file)) {
try {
const content = readFileSync(csv_file, 'utf-8');
const content = readFileSync(csv_file, "utf-8");
const result = parseFrontMatter(content);
existingFrontmatter = result.frontmatter;
csvContent = result.csvContent;
} catch (error) {
return {
success: false,
message: `读取现有文件失败:${error instanceof Error ? error.message : '未知错误'}`
message: `读取现有文件失败:${error instanceof Error ? error.message : "未知错误"}`,
};
}
}
// 合并或替换 frontmatter
const finalFrontmatter = merge && existingFrontmatter
? { ...existingFrontmatter, ...frontmatter }
: frontmatter;
const finalFrontmatter =
merge && existingFrontmatter
? { ...existingFrontmatter, ...frontmatter }
: frontmatter;
// 序列化 frontmatter
const frontmatterStr = serializeFrontMatter(finalFrontmatter);
@@ -100,32 +67,32 @@ export function writeFrontmatter(params: WriteFrontmatterParams): WriteFrontmatt
// 如果文件不存在或没有 CSV 内容,创建一个空的 CSV 内容
if (!csvContent.trim()) {
// 从 frontmatter 推断 CSV 表头
const headers = ['label'];
const headers = ["label"];
if (finalFrontmatter.fields && Array.isArray(finalFrontmatter.fields)) {
for (const field of finalFrontmatter.fields) {
if (field.name && typeof field.name === 'string') {
if (field.name && typeof field.name === "string") {
headers.push(field.name);
}
}
}
headers.push('body');
csvContent = headers.join(',') + '\n';
headers.push("body");
csvContent = headers.join(",") + "\n";
}
// 写入文件
try {
const fullContent = frontmatterStr + csvContent;
writeFileSync(csv_file, fullContent, 'utf-8');
writeFileSync(csv_file, fullContent, "utf-8");
return {
success: true,
message: `成功写入 frontmatter 到 ${csv_file}`,
csv_file
csv_file,
};
} catch (error) {
return {
success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
message: `写入失败:${error instanceof Error ? error.message : "未知错误"}`,
};
}
}