import { readFileSync, writeFileSync, existsSync } from 'fs'; import { dirname, join, basename, extname, relative } from 'path'; import { execSync } from 'child_process'; import yaml from 'js-yaml'; import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js'; /** * 预览 Deck 的参数 */ export interface PreviewDeckParams { /** * CSV 文件路径 */ csv_file: string; /** * Markdown 文件路径(可选,默认与 CSV 同名) */ md_file?: string; /** * 标题(可选,默认从 CSV 文件名推断) */ title?: string; /** * 描述(可选) */ description?: string; } /** * 预览 Deck 的结果 */ export interface PreviewDeckResult { success: boolean; message: string; md_file?: string; created?: boolean; 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) { try { // Windows 使用 start 命令 if (process.platform === 'win32') { execSync(`start "" "${url}"`, { stdio: 'ignore' }); } else if (process.platform === 'darwin') { execSync(`open "${url}"`, { stdio: 'ignore' }); } else { execSync(`xdg-open "${url}"`, { stdio: 'ignore' }); } console.error(`已打开浏览器:${url}`); } catch (error) { console.error('打开浏览器失败:', error); } } /** * 预览 Deck - 保存 Markdown 并打开浏览器 */ export function previewDeck(params: PreviewDeckParams): PreviewDeckResult { 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)}]`)) { // 仍然打开浏览器 const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/'); const previewUrl = `http://localhost:3000/${relativeMdPath}`; openBrowser(previewUrl); return { success: true, message: `预览文件 ${mdFilePath} 已存在`, md_file: mdFilePath, created: false, preview_url: previewUrl }; } } 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'); // 计算预览 URL const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/'); const previewUrl = `http://localhost:3000/${relativeMdPath.slice(0, -3)}`; // 打开浏览器 openBrowser(previewUrl); return { success: true, message: created ? `创建预览文件 ${mdFilePath}` : `更新预览文件 ${mdFilePath}`, md_file: mdFilePath, created, preview_url: previewUrl }; } catch (error) { return { success: false, message: `写入失败:${error instanceof Error ? error.message : '未知错误'}` }; } }