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

View File

@ -535,7 +535,8 @@ src/cli/
│ │ └── write-frontmatter.ts │ │ └── write-frontmatter.ts
│ ├── card/ │ ├── card/
│ │ └── card-crud.ts │ │ └── card-crud.ts
│ ├── ensure-deck-preview.ts │ ├── deck-utils.ts
│ ├── preview-deck.ts
│ └── generate-card-deck.ts │ └── generate-card-deck.ts
└── index.ts └── index.ts
``` ```

View File

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

134
src/cli/tools/deck-utils.ts Normal file
View File

@ -0,0 +1,134 @@
/**
* Shared deck utility functions used by generate-card-deck and preview-deck.
*/
import { dirname, join, basename, extname, relative } from "path";
import type {
DeckConfig,
DeckFrontmatter,
} from "./frontmatter/read-frontmatter.js";
import { parseFrontMatter } from "./frontmatter/shared.js";
import { readFileSync } from "fs";
/**
* Build a `:md-deck[path]{attrs}` directive string from a CSV path and config.
*/
export function buildDeckComponent(
csvPath: string,
config?: DeckConfig,
): string {
const parts = [`:md-deck[${csvPath}]`];
const attrs: string[] = [];
if (config?.size) {
attrs.push(`size="${config.size}"`);
}
if (config?.grid) {
attrs.push(`grid="${config.grid}"`);
}
if (config?.bleed !== undefined && config.bleed !== 1) {
attrs.push(`bleed="${config.bleed}"`);
}
if (config?.padding !== undefined && config.padding !== 2) {
attrs.push(`padding="${config.padding}"`);
}
if (config?.shape && config.shape !== "rectangle") {
attrs.push(`shape="${config.shape}"`);
}
if (config?.layers) {
attrs.push(`layers="${config.layers}"`);
}
if (config?.back_layers) {
attrs.push(`back-layers="${config.back_layers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(" ")}}`);
}
return parts.join("");
}
/**
* Derive a human-readable title from a CSV filename.
* e.g. "my-deck.csv" "My Deck"
*/
export function titleFromCsvPath(csvPath: string): string {
const name = basename(csvPath, extname(csvPath));
return name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
/**
* Compute the relative path from a markdown file to its CSV.
* Handles the case where they are in the same or different directories.
*/
export function relativeCsvPath(
mdFilePath: string,
csvFilePath: string,
): string {
const mdDir = dirname(mdFilePath);
const rel = relative(mdDir, csvFilePath);
return rel.startsWith(".") ? rel : `./${rel}`;
}
/**
* Determine the default markdown file path for a CSV.
* e.g. "foo/bar.csv" "foo/bar.md"
*/
export function defaultMdPath(csvFilePath: string): string {
const dir = dirname(csvFilePath);
const name = basename(csvFilePath, extname(csvFilePath));
return join(dir, `${name}.md`);
}
/**
* Read DeckConfig from a CSV file's frontmatter, if present.
*/
export function readDeckConfig(csvFilePath: string): DeckConfig | undefined {
try {
const content = readFileSync(csvFilePath, "utf-8");
const { frontmatter } = parseFrontMatter(content);
return frontmatter?.deck;
} catch {
return undefined;
}
}
/**
* Generate the standard markdown content for a deck preview file.
*/
export function generateDeckPreviewMd(
deckTitle: string,
deckComponent: string,
description?: string,
): string {
const lines: string[] = [];
lines.push(`# ${deckTitle}`);
lines.push("");
if (description) {
lines.push(description);
lines.push("");
}
lines.push("## 卡牌预览");
lines.push("");
lines.push(deckComponent);
lines.push("");
lines.push("## 使用说明");
lines.push("");
lines.push("- 点击卡牌可以查看详情");
lines.push("- 使用右上角的按钮可以随机抽取卡牌");
lines.push("- 可以通过编辑面板调整卡牌样式和布局");
lines.push("");
return lines.join("\n");
}

View File

@ -1,217 +0,0 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { dirname, join, basename, extname } from 'path';
import yaml from 'js-yaml';
import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js';
/**
* Deck
*/
export interface EnsureDeckPreviewParams {
/**
* CSV
*/
csv_file: string;
/**
* Markdown CSV
*/
md_file?: string;
/**
* CSV
*/
title?: string;
/**
*
*/
description?: string;
}
/**
* Deck
*/
export interface EnsureDeckPreviewResult {
success: boolean;
message: string;
md_file?: string;
created?: boolean;
}
/**
* 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 };
}
}
/**
* md-deck
*/
function buildDeckComponent(csvPath: string, config?: DeckConfig): string {
const parts = [`:md-deck[${csvPath}]`];
const attrs: string[] = [];
if (config?.size) {
attrs.push(`size="${config.size}"`);
}
if (config?.grid) {
attrs.push(`grid="${config.grid}"`);
}
if (config?.bleed !== undefined && config.bleed !== 1) {
attrs.push(`bleed="${config.bleed}"`);
}
if (config?.padding !== undefined && config.padding !== 2) {
attrs.push(`padding="${config.padding}"`);
}
if (config?.shape && config.shape !== 'rectangle') {
attrs.push(`shape="${config.shape}"`);
}
if (config?.layers) {
attrs.push(`layers="${config.layers}"`);
}
if (config?.back_layers) {
attrs.push(`back-layers="${config.back_layers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
}
/**
* Markdown
*/
export function ensureDeckPreview(params: EnsureDeckPreviewParams): EnsureDeckPreviewResult {
const { csv_file, md_file, title, description } = params;
// 检查 CSV 文件是否存在
if (!existsSync(csv_file)) {
return {
success: false,
message: `CSV 文件不存在:${csv_file}`
};
}
// 确定 Markdown 文件路径
let mdFilePath = md_file;
if (!mdFilePath) {
// 使用与 CSV 相同的路径和文件名,只是扩展名不同
const dir = dirname(csv_file);
const name = basename(csv_file, extname(csv_file));
mdFilePath = join(dir, `${name}.md`);
}
const created = !existsSync(mdFilePath);
// 如果文件已存在,检查是否已有 md-deck 组件
if (!created) {
try {
const existingContent = readFileSync(mdFilePath, 'utf-8');
// 如果已有 md-deck 组件引用该 CSV直接返回
if (existingContent.includes(`:md-deck[${csv_file}]`) ||
existingContent.includes(`:md-deck[./${basename(csv_file)}]`)) {
return {
success: true,
message: `预览文件 ${mdFilePath} 已存在`,
md_file: mdFilePath,
created: false
};
}
} catch (error) {
// 读取失败,继续创建
}
}
// 读取 CSV 的 frontmatter 获取配置
let deckConfig: DeckConfig | undefined;
try {
const csvContent = readFileSync(csv_file, 'utf-8');
const { frontmatter } = parseFrontMatter(csvContent);
deckConfig = frontmatter?.deck;
} catch (error) {
// 忽略错误,使用默认配置
}
// 确定标题
let deckTitle = title;
if (!deckTitle) {
// 从 CSV 文件名推断
const name = basename(csv_file, extname(csv_file));
deckTitle = name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
// 构建 md-deck 组件代码(使用相对路径)
const mdDir = dirname(mdFilePath);
const csvDir = dirname(csv_file);
let relativeCsvPath: string;
if (mdDir === csvDir) {
relativeCsvPath = `./${basename(csv_file)}`;
} else {
relativeCsvPath = `./${basename(csv_file)}`;
}
const deckComponent = buildDeckComponent(relativeCsvPath, deckConfig);
// 生成 Markdown 内容
const mdLines: string[] = [];
mdLines.push(`# ${deckTitle}`);
mdLines.push('');
if (description) {
mdLines.push(description);
mdLines.push('');
}
mdLines.push('## 卡牌预览');
mdLines.push('');
mdLines.push(deckComponent);
mdLines.push('');
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
// 写入文件
try {
writeFileSync(mdFilePath, mdLines.join('\n'), 'utf-8');
return {
success: true,
message: created
? `创建预览文件 ${mdFilePath}`
: `更新预览文件 ${mdFilePath}`,
md_file: mdFilePath,
created
};
} catch (error) {
return {
success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}

View File

@ -1,5 +1,5 @@
import { readFileSync, existsSync } from 'fs'; import { readFileSync, existsSync } from "fs";
import yaml from 'js-yaml'; import { parseFrontMatter } from "./shared.js";
/** /**
* CSV frontmatter * CSV frontmatter
@ -52,7 +52,7 @@ export interface CardFieldStyle {
/** /**
* "n" | "w" | "s" | "e"/西// * "n" | "w" | "s" | "e"/西//
*/ */
up?: 'n' | 'w' | 's' | 'e'; up?: "n" | "w" | "s" | "e";
} }
/** /**
@ -74,7 +74,7 @@ export interface DeckConfig {
grid?: string; grid?: string;
bleed?: number; bleed?: number;
padding?: number; padding?: number;
shape?: 'rectangle' | 'circle' | 'hex' | 'diamond'; shape?: "rectangle" | "circle" | "hex" | "diamond";
layers?: string; layers?: string;
back_layers?: string; back_layers?: string;
[key: string]: unknown; [key: string]: unknown;
@ -89,45 +89,25 @@ export interface ReadFrontmatterResult {
message: string; 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 * CSV frontmatter
*/ */
export function readFrontmatter(params: ReadFrontmatterParams): ReadFrontmatterResult { export function readFrontmatter(
params: ReadFrontmatterParams,
): ReadFrontmatterResult {
const { csv_file } = params; const { csv_file } = params;
// 检查文件是否存在 // 检查文件是否存在
if (!existsSync(csv_file)) { if (!existsSync(csv_file)) {
return { return {
success: false, success: false,
message: `文件不存在:${csv_file}` message: `文件不存在:${csv_file}`,
}; };
} }
try { try {
// 读取文件内容 // 读取文件内容
const content = readFileSync(csv_file, 'utf-8'); const content = readFileSync(csv_file, "utf-8");
// 解析 frontmatter // 解析 frontmatter
const { frontmatter } = parseFrontMatter(content); const { frontmatter } = parseFrontMatter(content);
@ -136,19 +116,19 @@ export function readFrontmatter(params: ReadFrontmatterParams): ReadFrontmatterR
return { return {
success: true, success: true,
frontmatter: {}, frontmatter: {},
message: `文件 ${csv_file} 没有 frontmatter返回空对象` message: `文件 ${csv_file} 没有 frontmatter返回空对象`,
}; };
} }
return { return {
success: true, success: true,
frontmatter, frontmatter,
message: `成功读取 ${csv_file} 的 frontmatter` message: `成功读取 ${csv_file} 的 frontmatter`,
}; };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `读取失败:${error instanceof Error ? error.message : '未知错误'}` message: `读取失败:${error instanceof Error ? error.message : "未知错误"}`,
}; };
} }
} }

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`;
}

View File

@ -1,6 +1,6 @@
import { readFileSync, writeFileSync, existsSync } from 'fs'; import { readFileSync, writeFileSync, existsSync } from "fs";
import yaml from 'js-yaml'; import type { DeckFrontmatter } from "./read-frontmatter.js";
import type { DeckFrontmatter } from './read-frontmatter.js'; import { parseFrontMatter, serializeFrontMatter } from "./shared.js";
/** /**
* CSV frontmatter * CSV frontmatter
@ -29,70 +29,37 @@ export interface WriteFrontmatterResult {
csv_file?: string; 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 * / CSV frontmatter
*/ */
export function writeFrontmatter(params: WriteFrontmatterParams): WriteFrontmatterResult { export function writeFrontmatter(
params: WriteFrontmatterParams,
): WriteFrontmatterResult {
const { csv_file, frontmatter, merge = true } = params; const { csv_file, frontmatter, merge = true } = params;
let csvContent = ''; let csvContent = "";
let existingFrontmatter: DeckFrontmatter | undefined; let existingFrontmatter: DeckFrontmatter | undefined;
// 如果文件存在且需要合并,先读取现有内容 // 如果文件存在且需要合并,先读取现有内容
if (merge && existsSync(csv_file)) { if (merge && existsSync(csv_file)) {
try { try {
const content = readFileSync(csv_file, 'utf-8'); const content = readFileSync(csv_file, "utf-8");
const result = parseFrontMatter(content); const result = parseFrontMatter(content);
existingFrontmatter = result.frontmatter; existingFrontmatter = result.frontmatter;
csvContent = result.csvContent; csvContent = result.csvContent;
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `读取现有文件失败:${error instanceof Error ? error.message : '未知错误'}` message: `读取现有文件失败:${error instanceof Error ? error.message : "未知错误"}`,
}; };
} }
} }
// 合并或替换 frontmatter // 合并或替换 frontmatter
const finalFrontmatter = merge && existingFrontmatter const finalFrontmatter =
? { ...existingFrontmatter, ...frontmatter } merge && existingFrontmatter
: frontmatter; ? { ...existingFrontmatter, ...frontmatter }
: frontmatter;
// 序列化 frontmatter // 序列化 frontmatter
const frontmatterStr = serializeFrontMatter(finalFrontmatter); const frontmatterStr = serializeFrontMatter(finalFrontmatter);
@ -100,32 +67,32 @@ export function writeFrontmatter(params: WriteFrontmatterParams): WriteFrontmatt
// 如果文件不存在或没有 CSV 内容,创建一个空的 CSV 内容 // 如果文件不存在或没有 CSV 内容,创建一个空的 CSV 内容
if (!csvContent.trim()) { if (!csvContent.trim()) {
// 从 frontmatter 推断 CSV 表头 // 从 frontmatter 推断 CSV 表头
const headers = ['label']; const headers = ["label"];
if (finalFrontmatter.fields && Array.isArray(finalFrontmatter.fields)) { if (finalFrontmatter.fields && Array.isArray(finalFrontmatter.fields)) {
for (const field of 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(field.name);
} }
} }
} }
headers.push('body'); headers.push("body");
csvContent = headers.join(',') + '\n'; csvContent = headers.join(",") + "\n";
} }
// 写入文件 // 写入文件
try { try {
const fullContent = frontmatterStr + csvContent; const fullContent = frontmatterStr + csvContent;
writeFileSync(csv_file, fullContent, 'utf-8'); writeFileSync(csv_file, fullContent, "utf-8");
return { return {
success: true, success: true,
message: `成功写入 frontmatter 到 ${csv_file}`, message: `成功写入 frontmatter 到 ${csv_file}`,
csv_file csv_file,
}; };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}` message: `写入失败:${error instanceof Error ? error.message : "未知错误"}`,
}; };
} }
} }

View File

@ -1,5 +1,7 @@
import { writeFileSync, mkdirSync, existsSync } from 'fs'; import { writeFileSync, mkdirSync, existsSync } from "fs";
import { join } from 'path'; import { join } from "path";
import { buildDeckComponent, generateDeckPreviewMd } from "./deck-utils.js";
import type { DeckConfig } from "./frontmatter/read-frontmatter.js";
/** /**
* *
@ -24,7 +26,7 @@ export interface CardFieldStyle {
/** /**
* "n" | "w" | "s" | "e"/西// * "n" | "w" | "s" | "e"/西//
*/ */
up?: 'n' | 'w' | 's' | 'e'; up?: "n" | "w" | "s" | "e";
} }
/** /**
@ -45,18 +47,7 @@ export interface CardTemplate {
examples?: Record<string, string>[]; examples?: Record<string, string>[];
} }
/** export type { DeckConfig };
* Deck
*/
export interface DeckConfig {
size?: string;
grid?: string;
bleed?: number;
padding?: number;
shape?: 'rectangle' | 'circle' | 'hex' | 'diamond';
layers?: string;
backLayers?: string;
}
/** /**
* *
@ -73,31 +64,31 @@ export interface GenerateCardDeckParams {
/** /**
* CSV * CSV
*/ */
function generateCardCSV( function generateCardCSV(template: CardTemplate, cardCount: number): string {
template: CardTemplate,
cardCount: number
): string {
const fields = template.fields; const fields = template.fields;
// 构建 CSV 表头 // 构建 CSV 表头
const headers = ['label', ...fields.map(f => f.name), 'body']; const headers = ["label", ...fields.map((f) => f.name), "body"];
// 生成示例数据 // 生成示例数据
const rows: string[][] = []; const rows: string[][] = [];
const examples = template.examples || []; const examples = template.examples || [];
for (let i = 0; i < cardCount; i++) { for (let i = 0; i < cardCount; i++) {
const row: string[] = [(i + 1).toString()]; const row: string[] = [(i + 1).toString()];
// 为每个字段生成值 // 为每个字段生成值
for (const field of fields) { for (const field of fields) {
let value = ''; let value = "";
if (examples.length > 0) { if (examples.length > 0) {
// 从示例中循环取值 // 从示例中循环取值
const exampleIndex = i % examples.length; const exampleIndex = i % examples.length;
const example = examples[exampleIndex]; const example = examples[exampleIndex];
value = example[field.name] || field.examples?.[i % (field.examples?.length || 1)] || ''; value =
example[field.name] ||
field.examples?.[i % (field.examples?.length || 1)] ||
"";
} else if (field.examples && field.examples.length > 0) { } else if (field.examples && field.examples.length > 0) {
// 从字段的示例中取值 // 从字段的示例中取值
value = field.examples[i % field.examples.length]; value = field.examples[i % field.examples.length];
@ -105,131 +96,49 @@ function generateCardCSV(
// 默认占位符 // 默认占位符
value = `{{${field.name}_${i + 1}}}`; value = `{{${field.name}_${i + 1}}}`;
} }
row.push(value); row.push(value);
} }
// body 列使用模板语法 // body 列使用模板语法
const bodyParts: string[] = []; const bodyParts: string[] = [];
for (const field of fields) { for (const field of fields) {
bodyParts.push(`**${field.name}:** {{${field.name}}}`); bodyParts.push(`**${field.name}:** {{${field.name}}}`);
} }
row.push(bodyParts.join('\n\n')); row.push(bodyParts.join("\n\n"));
rows.push(row); rows.push(row);
} }
// 组合 CSV 内容 // 组合 CSV 内容
const csvLines = [headers.join(',')]; const csvLines = [headers.join(",")];
for (const row of rows) { for (const row of rows) {
csvLines.push(row.map(cell => { csvLines.push(
// 处理包含逗号或换行的单元格 row
if (cell.includes(',') || cell.includes('\n') || cell.includes('"')) { .map((cell) => {
return `"${cell.replace(/"/g, '""')}"`; // 处理包含逗号或换行的单元格
} if (cell.includes(",") || cell.includes("\n") || cell.includes('"')) {
return cell; return `"${cell.replace(/"/g, '""')}"`;
}).join(',')); }
return cell;
})
.join(","),
);
} }
return csvLines.join('\n');
}
/** return csvLines.join("\n");
* Markdown
*/
function generateDeckMarkdown(
deckName: string,
csvFileName: string,
deckConfig: DeckConfig,
description?: string
): string {
const mdLines: string[] = [];
// 标题
mdLines.push(`# ${deckName}`);
mdLines.push('');
// 描述
if (description) {
mdLines.push(description);
mdLines.push('');
}
// 卡牌预览组件
mdLines.push('## 卡牌预览');
mdLines.push('');
// 构建 :md-deck 组件代码
const deckComponent = buildDeckComponent(csvFileName, deckConfig);
mdLines.push(deckComponent);
mdLines.push('');
// 使用说明
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
return mdLines.join('\n');
}
/**
* 构建 :md-deck
*/
function buildDeckComponent(
csvFileName: string,
config: DeckConfig
): string {
const parts = [`:md-deck[${csvFileName}]`];
const attrs: string[] = [];
if (config.size) {
attrs.push(`size="${config.size}"`);
}
if (config.grid) {
attrs.push(`grid="${config.grid}"`);
}
if (config.bleed !== undefined && config.bleed !== 1) {
attrs.push(`bleed="${config.bleed}"`);
}
if (config.padding !== undefined && config.padding !== 2) {
attrs.push(`padding="${config.padding}"`);
}
if (config.shape && config.shape !== 'rectangle') {
attrs.push(`shape="${config.shape}"`);
}
if (config.layers) {
attrs.push(`layers="${config.layers}"`);
}
if (config.backLayers) {
attrs.push(`back-layers="${config.backLayers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
} }
/** /**
* *
*/ */
function autoGenerateLayers(fields: CardField[]): string { function autoGenerateLayers(fields: CardField[]): string {
if (fields.length === 0) return ''; if (fields.length === 0) return "";
const layers: string[] = []; const layers: string[] = [];
const totalHeight = 8; const totalHeight = 8;
const heightPerField = Math.floor((totalHeight - 2) / fields.length); const heightPerField = Math.floor((totalHeight - 2) / fields.length);
for (let i = 0; i < fields.length; i++) { for (let i = 0; i < fields.length; i++) {
const field = fields[i]; const field = fields[i];
const y1 = 2 + i * heightPerField; const y1 = 2 + i * heightPerField;
@ -237,8 +146,8 @@ function autoGenerateLayers(fields: CardField[]): string {
const fontSize = Math.min(12, Math.floor(80 / fields.length)); const fontSize = Math.min(12, Math.floor(80 / fields.length));
layers.push(`${field.name}:1,${y1}-${y2},${fontSize}`); layers.push(`${field.name}:1,${y1}-${y2},${fontSize}`);
} }
return layers.join(' '); return layers.join(" ");
} }
/** /**
@ -256,61 +165,66 @@ export function generateCardDeck(params: GenerateCardDeckParams): {
card_count = 10, card_count = 10,
card_template, card_template,
deck_config = {}, deck_config = {},
description description,
} = params; } = params;
// 确保输出目录存在 // 确保输出目录存在
if (!existsSync(output_dir)) { if (!existsSync(output_dir)) {
mkdirSync(output_dir, { recursive: true }); mkdirSync(output_dir, { recursive: true });
} }
// 生成文件名 // 生成文件名
const safeName = deck_name.toLowerCase().replace(/\s+/g, '-'); const safeName = deck_name.toLowerCase().replace(/\s+/g, "-");
const csvFileName = `${safeName}.csv`; const csvFileName = `${safeName}.csv`;
const mdFileName = `${safeName}.md`; const mdFileName = `${safeName}.md`;
// 创建默认模板(如果没有提供) // 创建默认模板(如果没有提供)
const template: CardTemplate = card_template || { const template: CardTemplate = card_template || {
fields: [ fields: [
{ name: 'name', description: '卡牌名称', examples: ['示例卡牌 1', '示例卡牌 2'] }, {
{ name: 'type', description: '卡牌类型', examples: ['物品', '法术'] }, name: "name",
{ name: 'cost', description: '费用', examples: ['1', '2'] }, description: "卡牌名称",
{ name: 'description', description: '效果描述', examples: ['这是一个效果描述', '这是另一个效果'] } examples: ["示例卡牌 1", "示例卡牌 2"],
] },
{ name: "type", description: "卡牌类型", examples: ["物品", "法术"] },
{ name: "cost", description: "费用", examples: ["1", "2"] },
{
name: "description",
description: "效果描述",
examples: ["这是一个效果描述", "这是另一个效果"],
},
],
}; };
// 创建默认配置(如果没有提供) // 创建默认配置(如果没有提供)
const config: DeckConfig = { const config: DeckConfig = {
size: deck_config.size || '54x86', size: deck_config.size || "54x86",
grid: deck_config.grid || '5x8', grid: deck_config.grid || "5x8",
bleed: deck_config.bleed ?? 1, bleed: deck_config.bleed ?? 1,
padding: deck_config.padding ?? 2, padding: deck_config.padding ?? 2,
shape: deck_config.shape || 'rectangle', shape: deck_config.shape || "rectangle",
layers: deck_config.layers || autoGenerateLayers(template.fields) layers: deck_config.layers || autoGenerateLayers(template.fields),
}; };
// 生成 CSV 内容 // 生成 CSV 内容
const csvContent = generateCardCSV(template, card_count); const csvContent = generateCardCSV(template, card_count);
const csvPath = join(output_dir, csvFileName); const csvPath = join(output_dir, csvFileName);
writeFileSync(csvPath, csvContent, 'utf-8'); writeFileSync(csvPath, csvContent, "utf-8");
// 生成 Markdown 内容 // 生成 Markdown 内容
const mdContent = generateDeckMarkdown( const deckComponent = buildDeckComponent(`./${csvFileName}`, config);
const mdContent = generateDeckPreviewMd(
deck_name, deck_name,
`./${csvFileName}`, deckComponent,
config, description,
description
); );
const mdPath = join(output_dir, mdFileName); const mdPath = join(output_dir, mdFileName);
writeFileSync(mdPath, mdContent, 'utf-8'); writeFileSync(mdPath, mdContent, "utf-8");
// 构建完整的 deck 组件代码
const deckComponent = buildDeckComponent(`./${csvFileName}`, config);
return { return {
mdFile: mdPath, mdFile: mdPath,
csvFile: csvPath, csvFile: csvPath,
deckComponent, deckComponent,
message: `已生成卡牌组 "${deck_name}":\n- Markdown 文件:${mdPath}\n- CSV 数据:${csvPath}\n- 组件代码:${deckComponent}` message: `已生成卡牌组 "${deck_name}":\n- Markdown 文件:${mdPath}\n- CSV 数据:${csvPath}\n- 组件代码:${deckComponent}`,
}; };
} }

View File

@ -1,8 +1,13 @@
import { readFileSync, writeFileSync, existsSync } from 'fs'; import { readFileSync, writeFileSync, existsSync } from "fs";
import { dirname, join, basename, extname, relative } from 'path'; import { relative } from "path";
import { execSync } from 'child_process'; import { execSync } from "child_process";
import yaml from 'js-yaml'; import {
import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js'; buildDeckComponent,
titleFromCsvPath,
defaultMdPath,
readDeckConfig,
generateDeckPreviewMd,
} from "./deck-utils.js";
/** /**
* Deck * Deck
@ -37,88 +42,35 @@ export interface PreviewDeckResult {
preview_url?: string; preview_url?: string;
} }
/**
* 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 };
}
}
/**
* md-deck
*/
function buildDeckComponent(csvPath: string, config?: DeckConfig): string {
const parts = [`:md-deck[${csvPath}]`];
const attrs: string[] = [];
if (config?.size) {
attrs.push(`size="${config.size}"`);
}
if (config?.grid) {
attrs.push(`grid="${config.grid}"`);
}
if (config?.bleed !== undefined && config.bleed !== 1) {
attrs.push(`bleed="${config.bleed}"`);
}
if (config?.padding !== undefined && config.padding !== 2) {
attrs.push(`padding="${config.padding}"`);
}
if (config?.shape && config.shape !== 'rectangle') {
attrs.push(`shape="${config.shape}"`);
}
if (config?.layers) {
attrs.push(`layers="${config.layers}"`);
}
if (config?.back_layers) {
attrs.push(`back-layers="${config.back_layers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
}
/** /**
* *
*/ */
function openBrowser(url: string) { function openBrowser(url: string) {
try { try {
// Windows 使用 start 命令 if (process.platform === "win32") {
if (process.platform === 'win32') { execSync(`start "" "${url}"`, { stdio: "ignore" });
execSync(`start "" "${url}"`, { stdio: 'ignore' }); } else if (process.platform === "darwin") {
} else if (process.platform === 'darwin') { execSync(`open "${url}"`, { stdio: "ignore" });
execSync(`open "${url}"`, { stdio: 'ignore' });
} else { } else {
execSync(`xdg-open "${url}"`, { stdio: 'ignore' }); execSync(`xdg-open "${url}"`, { stdio: "ignore" });
} }
console.error(`已打开浏览器:${url}`); console.error(`已打开浏览器:${url}`);
} catch (error) { } catch (error) {
console.error('打开浏览器失败:', error); console.error("打开浏览器失败:", error);
} }
} }
/**
* URL
*/
function computePreviewUrl(mdFilePath: string): string {
const relativeMdPath = relative(process.cwd(), mdFilePath)
.split("\\")
.join("/")
.replace(/\.md$/, "");
return `http://localhost:3000/${relativeMdPath}`;
}
/** /**
* Deck - Markdown * Deck - Markdown
*/ */
@ -129,108 +81,61 @@ export function previewDeck(params: PreviewDeckParams): PreviewDeckResult {
if (!existsSync(csv_file)) { if (!existsSync(csv_file)) {
return { return {
success: false, success: false,
message: `CSV 文件不存在:${csv_file}` message: `CSV 文件不存在:${csv_file}`,
}; };
} }
// 确定 Markdown 文件路径 // 确定 Markdown 文件路径
let mdFilePath = md_file; const mdFilePath = md_file || defaultMdPath(csv_file);
if (!mdFilePath) {
// 使用与 CSV 相同的路径和文件名,只是扩展名不同
const dir = dirname(csv_file);
const name = basename(csv_file, extname(csv_file));
mdFilePath = join(dir, `${name}.md`);
}
const created = !existsSync(mdFilePath); const created = !existsSync(mdFilePath);
// 如果文件已存在,检查是否已有 md-deck 组件 // 如果文件已存在,检查是否已有 md-deck 组件
if (!created) { if (!created) {
try { try {
const existingContent = readFileSync(mdFilePath, 'utf-8'); const existingContent = readFileSync(mdFilePath, "utf-8");
// 如果已有 md-deck 组件引用该 CSV直接返回 if (
if (existingContent.includes(`:md-deck[${csv_file}]`) || existingContent.includes(`:md-deck[${csv_file}]`) ||
existingContent.includes(`:md-deck[./${basename(csv_file)}]`)) { existingContent.includes(`:md-deck[./${csv_file.split("/").pop()}]`)
// 仍然打开浏览器 ) {
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/'); const previewUrl = computePreviewUrl(mdFilePath);
const previewUrl = `http://localhost:3000/${relativeMdPath}`;
openBrowser(previewUrl); openBrowser(previewUrl);
return { return {
success: true, success: true,
message: `预览文件 ${mdFilePath} 已存在`, message: `预览文件 ${mdFilePath} 已存在`,
md_file: mdFilePath, md_file: mdFilePath,
created: false, created: false,
preview_url: previewUrl preview_url: previewUrl,
}; };
} }
} catch (error) { } catch {
// 读取失败,继续创建 // 读取失败,继续创建
} }
} }
// 读取 CSV 的 frontmatter 获取配置 // 读取 CSV 的 frontmatter 获取配置
let deckConfig: DeckConfig | undefined; const deckConfig = readDeckConfig(csv_file);
try {
const csvContent = readFileSync(csv_file, 'utf-8');
const { frontmatter } = parseFrontMatter(csvContent);
deckConfig = frontmatter?.deck;
} catch (error) {
// 忽略错误,使用默认配置
}
// 确定标题 // 确定标题
let deckTitle = title; const deckTitle = title || titleFromCsvPath(csv_file);
if (!deckTitle) {
// 从 CSV 文件名推断
const name = basename(csv_file, extname(csv_file));
deckTitle = name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
// 构建 md-deck 组件代码(使用相对路径) // 构建 md-deck 组件代码
const mdDir = dirname(mdFilePath); const deckComponent = buildDeckComponent(
const csvDir = dirname(csv_file); `./${csv_file.split("/").pop()}`,
let relativeCsvPath: string; deckConfig,
);
if (mdDir === csvDir) {
relativeCsvPath = `./${basename(csv_file)}`;
} else {
relativeCsvPath = `./${basename(csv_file)}`;
}
const deckComponent = buildDeckComponent(relativeCsvPath, deckConfig);
// 生成 Markdown 内容 // 生成 Markdown 内容
const mdLines: string[] = []; const mdContent = generateDeckPreviewMd(
deckTitle,
mdLines.push(`# ${deckTitle}`); deckComponent,
mdLines.push(''); description,
);
if (description) {
mdLines.push(description);
mdLines.push('');
}
mdLines.push('## 卡牌预览');
mdLines.push('');
mdLines.push(deckComponent);
mdLines.push('');
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
// 写入文件 // 写入文件
try { try {
writeFileSync(mdFilePath, mdLines.join('\n'), 'utf-8'); writeFileSync(mdFilePath, mdContent, "utf-8");
// 计算预览 URL const previewUrl = computePreviewUrl(mdFilePath);
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/');
const previewUrl = `http://localhost:3000/${relativeMdPath.slice(0, -3)}`;
// 打开浏览器
openBrowser(previewUrl); openBrowser(previewUrl);
return { return {
@ -240,12 +145,12 @@ export function previewDeck(params: PreviewDeckParams): PreviewDeckResult {
: `更新预览文件 ${mdFilePath}`, : `更新预览文件 ${mdFilePath}`,
md_file: mdFilePath, md_file: mdFilePath,
created, created,
preview_url: previewUrl preview_url: previewUrl,
}; };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}` message: `写入失败:${error instanceof Error ? error.message : "未知错误"}`,
}; };
} }
} }