markdown-to-document-cli 1.1.4 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +54 -9
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +476 -103
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +18 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/services/CoverService.d.ts +25 -3
  10. package/dist/services/CoverService.d.ts.map +1 -1
  11. package/dist/services/CoverService.js +226 -97
  12. package/dist/services/CoverService.js.map +1 -1
  13. package/dist/services/MarkdownPreprocessor.d.ts +5 -0
  14. package/dist/services/MarkdownPreprocessor.d.ts.map +1 -1
  15. package/dist/services/MarkdownPreprocessor.js +18 -7
  16. package/dist/services/MarkdownPreprocessor.js.map +1 -1
  17. package/dist/services/PandocService.d.ts +3 -1
  18. package/dist/services/PandocService.d.ts.map +1 -1
  19. package/dist/services/PandocService.js +31 -15
  20. package/dist/services/PandocService.js.map +1 -1
  21. package/dist/services/TypographyService.d.ts +8 -1
  22. package/dist/services/TypographyService.d.ts.map +1 -1
  23. package/dist/services/TypographyService.js +417 -57
  24. package/dist/services/TypographyService.js.map +1 -1
  25. package/dist/types/index.d.ts +37 -11
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/index.js +28 -1
  28. package/dist/types/index.js.map +1 -1
  29. package/dist/utils/constants.d.ts.map +1 -1
  30. package/dist/utils/constants.js +224 -0
  31. package/dist/utils/constants.js.map +1 -1
  32. package/dist/utils/cssBuilder.d.ts +93 -0
  33. package/dist/utils/cssBuilder.d.ts.map +1 -0
  34. package/dist/utils/cssBuilder.js +231 -0
  35. package/dist/utils/cssBuilder.js.map +1 -0
  36. package/dist/utils/themeUtils.d.ts +70 -0
  37. package/dist/utils/themeUtils.d.ts.map +1 -0
  38. package/dist/utils/themeUtils.js +141 -0
  39. package/dist/utils/themeUtils.js.map +1 -0
  40. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2,9 +2,12 @@
2
2
  /**
3
3
  * Markdown to Document CLI
4
4
  *
5
+ * Refactored for improved UX with streamlined Interactive Mode
6
+ *
5
7
  * Usage:
6
8
  * npx markdown-to-document-cli <input.md>
7
9
  * m2d <input.md> [options]
10
+ * m2d interactive (or m2d i)
8
11
  */
9
12
  import { Command } from 'commander';
10
13
  import chalk from 'chalk';
@@ -15,23 +18,237 @@ import { TYPOGRAPHY_PRESETS, COVER_THEMES } from './utils/constants.js';
15
18
  import { Logger } from './utils/common.js';
16
19
  import * as path from 'path';
17
20
  import * as fs from 'fs';
21
+ import { fileURLToPath } from 'url';
18
22
  const program = new Command();
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+ const getCliVersion = () => {
26
+ try {
27
+ const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
28
+ const raw = fs.readFileSync(packageJsonPath, 'utf-8');
29
+ const parsed = JSON.parse(raw);
30
+ return parsed.version || '0.0.0';
31
+ }
32
+ catch {
33
+ return '0.0.0';
34
+ }
35
+ };
36
+ /**
37
+ * Analyze markdown content for Obsidian syntax and output optimization needs
38
+ */
39
+ function analyzeMarkdownContent(content) {
40
+ const result = {
41
+ hasObsidianImages: false,
42
+ hasObsidianLinks: false,
43
+ hasHighlights: false,
44
+ hasCallouts: false,
45
+ hasLongCodeLines: false,
46
+ hasComplexTables: false,
47
+ hasMultipleH1: false,
48
+ hasFrontmatter: false,
49
+ imageCount: 0,
50
+ tableCount: 0,
51
+ codeBlockCount: 0,
52
+ wordCount: 0,
53
+ recommendPreprocess: false,
54
+ recommendedPreset: 'ebook',
55
+ issues: [],
56
+ };
57
+ // Check for YAML frontmatter
58
+ result.hasFrontmatter = /^---\n[\s\S]*?\n---/.test(content);
59
+ // Check for Obsidian image syntax: ![[image]]
60
+ const obsidianImageMatches = content.match(/!\[\[([^\]]+)\]\]/g);
61
+ result.hasObsidianImages = !!obsidianImageMatches;
62
+ if (obsidianImageMatches) {
63
+ result.issues.push(`Obsidian 이미지 문법 ${obsidianImageMatches.length}개 발견`);
64
+ }
65
+ // Check for Obsidian internal links: [[link]]
66
+ const obsidianLinkMatches = content.match(/(?<!!)\[\[([^\]]+)\]\]/g);
67
+ result.hasObsidianLinks = !!obsidianLinkMatches;
68
+ if (obsidianLinkMatches) {
69
+ result.issues.push(`Obsidian 내부 링크 ${obsidianLinkMatches.length}개 발견`);
70
+ }
71
+ // Check for highlights: ==text==
72
+ const highlightMatches = content.match(/==([^=]+)==/g);
73
+ result.hasHighlights = !!highlightMatches;
74
+ if (highlightMatches) {
75
+ result.issues.push(`하이라이트 문법 ${highlightMatches.length}개 발견`);
76
+ }
77
+ // Check for callouts: > [!type]
78
+ const calloutMatches = content.match(/>\s*\[!(\w+)\]/g);
79
+ result.hasCallouts = !!calloutMatches;
80
+ if (calloutMatches) {
81
+ result.issues.push(`콜아웃 ${calloutMatches.length}개 발견`);
82
+ }
83
+ // Count images (standard markdown)
84
+ const standardImageMatches = content.match(/!\[([^\]]*)\]\([^)]+\)/g);
85
+ result.imageCount = (obsidianImageMatches?.length || 0) + (standardImageMatches?.length || 0);
86
+ // Count tables
87
+ const tableMatches = content.match(/\|.*\|.*\n\|[-:| ]+\|/g);
88
+ result.tableCount = tableMatches?.length || 0;
89
+ // Check for complex tables (>5 columns or very long cells)
90
+ if (tableMatches) {
91
+ for (const table of tableMatches) {
92
+ const columns = (table.match(/\|/g)?.length || 0) - 1;
93
+ if (columns > 5) {
94
+ result.hasComplexTables = true;
95
+ result.issues.push('5열 초과 복잡한 표 발견');
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ // Count code blocks and check for long lines
101
+ const codeBlockMatches = content.match(/```[\s\S]*?```/g);
102
+ result.codeBlockCount = codeBlockMatches?.length || 0;
103
+ if (codeBlockMatches) {
104
+ for (const block of codeBlockMatches) {
105
+ const lines = block.split('\n');
106
+ for (const line of lines) {
107
+ if (line.length > 100) {
108
+ result.hasLongCodeLines = true;
109
+ result.issues.push('100자 초과 코드 라인 발견 (PDF 잘림 위험)');
110
+ break;
111
+ }
112
+ }
113
+ if (result.hasLongCodeLines)
114
+ break;
115
+ }
116
+ }
117
+ // Check for multiple H1
118
+ const h1Matches = content.match(/^#\s+[^\n]+/gm);
119
+ result.hasMultipleH1 = (h1Matches?.length || 0) > 1;
120
+ if (result.hasMultipleH1) {
121
+ result.issues.push(`H1 제목 ${h1Matches?.length}개 발견 (1개 권장)`);
122
+ }
123
+ // Word count (rough estimate)
124
+ const textOnly = content.replace(/```[\s\S]*?```/g, '').replace(/[#*`\[\]()]/g, '');
125
+ result.wordCount = textOnly.split(/\s+/).filter(w => w.length > 0).length;
126
+ // Determine if preprocessing is recommended
127
+ result.recommendPreprocess =
128
+ result.hasObsidianImages ||
129
+ result.hasObsidianLinks ||
130
+ result.hasHighlights ||
131
+ result.hasCallouts ||
132
+ result.hasLongCodeLines ||
133
+ result.hasComplexTables ||
134
+ result.hasMultipleH1;
135
+ // Recommend typography preset based on content analysis
136
+ if (result.imageCount > 10) {
137
+ result.recommendedPreset = 'image_heavy';
138
+ }
139
+ else if (result.tableCount > 5) {
140
+ result.recommendedPreset = 'table_heavy';
141
+ }
142
+ else if (result.codeBlockCount > 10) {
143
+ result.recommendedPreset = 'manual';
144
+ }
145
+ else if (result.wordCount > 10000) {
146
+ result.recommendedPreset = 'text_heavy';
147
+ }
148
+ else {
149
+ result.recommendedPreset = 'balanced';
150
+ }
151
+ return result;
152
+ }
153
+ /**
154
+ * Display analysis result to console
155
+ */
156
+ function displayAnalysisResult(result) {
157
+ console.log(chalk.bold('📊 문서 분석 결과:\n'));
158
+ // Statistics
159
+ console.log(chalk.gray(' 📝 단어 수:'), chalk.cyan(`약 ${result.wordCount.toLocaleString()}개`));
160
+ console.log(chalk.gray(' 🖼️ 이미지:'), chalk.cyan(`${result.imageCount}개`));
161
+ console.log(chalk.gray(' 📊 표:'), chalk.cyan(`${result.tableCount}개`));
162
+ console.log(chalk.gray(' 💻 코드 블록:'), chalk.cyan(`${result.codeBlockCount}개`));
163
+ console.log(chalk.gray(' 📋 Frontmatter:'), result.hasFrontmatter ? chalk.green('있음') : chalk.yellow('없음'));
164
+ // Issues found
165
+ if (result.issues.length > 0) {
166
+ console.log(chalk.yellow('\n⚠️ 발견된 이슈:'));
167
+ result.issues.forEach(issue => {
168
+ console.log(chalk.yellow(` • ${issue}`));
169
+ });
170
+ }
171
+ else {
172
+ console.log(chalk.green('\n✅ 특별한 이슈 없음 - 표준 Markdown'));
173
+ }
174
+ // Recommendation
175
+ console.log(chalk.bold('\n💡 권장 사항:'));
176
+ if (result.recommendPreprocess) {
177
+ console.log(chalk.green(' → 문서 최적화가 필요하지만, 변환 과정에서 자동으로 적용됩니다.'));
178
+ }
179
+ else {
180
+ console.log(chalk.blue(' → 바로 변환해도 안정적입니다.'));
181
+ }
182
+ console.log(chalk.gray(` → 추천 프리셋: ${result.recommendedPreset}`));
183
+ }
184
+ /**
185
+ * Get typography preset choices with recommended preset highlighted
186
+ */
187
+ function getTypographyPresetChoices(analysisResult) {
188
+ const presetCategories = {
189
+ 'Basic': ['novel', 'presentation', 'review', 'ebook'],
190
+ 'Content-focused': ['text_heavy', 'table_heavy', 'image_heavy', 'balanced'],
191
+ 'Document Type': ['report', 'manual', 'magazine'],
192
+ };
193
+ const choices = [];
194
+ for (const [category, presetIds] of Object.entries(presetCategories)) {
195
+ choices.push(new inquirer.Separator(chalk.bold(`\n── ${category} ──`)));
196
+ for (const presetId of presetIds) {
197
+ const preset = TYPOGRAPHY_PRESETS[presetId];
198
+ if (preset) {
199
+ const isRecommended = presetId === analysisResult.recommendedPreset;
200
+ const name = isRecommended
201
+ ? chalk.green(`★ ${preset.name}`) + chalk.gray(` - ${preset.description}`) + chalk.green(' (권장)')
202
+ : chalk.cyan(preset.name) + chalk.gray(` - ${preset.description}`);
203
+ choices.push({ name, value: presetId });
204
+ }
205
+ }
206
+ }
207
+ return choices;
208
+ }
209
+ /**
210
+ * Get cover theme choices grouped by category
211
+ */
212
+ function getCoverThemeChoices() {
213
+ const themeCategories = {
214
+ 'Basic': ['apple', 'modern_gradient', 'dark_tech', 'nature', 'classic_book', 'minimalist'],
215
+ 'Professional': ['corporate', 'academic', 'magazine'],
216
+ 'Creative': ['sunset', 'ocean', 'aurora', 'rose_gold'],
217
+ 'Seasonal': ['spring', 'autumn', 'winter'],
218
+ };
219
+ const choices = [];
220
+ for (const [category, themeIds] of Object.entries(themeCategories)) {
221
+ choices.push(new inquirer.Separator(chalk.bold(`\n── ${category} ──`)));
222
+ for (const themeId of themeIds) {
223
+ const theme = COVER_THEMES[themeId];
224
+ if (theme) {
225
+ choices.push({
226
+ name: chalk.cyan(theme.name) + chalk.gray(` - ${theme.description}`),
227
+ value: themeId,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ return choices;
233
+ }
19
234
  // Configure CLI
20
235
  program
21
236
  .name('markdown-to-document')
22
237
  .alias('m2d')
23
238
  .description('Professional-grade EPUB/PDF conversion tool for Markdown files')
24
- .version('1.0.0')
239
+ .version(getCliVersion())
25
240
  .argument('<input>', 'Input markdown file path')
241
+ .option('--title <title>', 'Book title (defaults to frontmatter title or filename)')
242
+ .option('--author <author>', 'Author name (defaults to frontmatter author)')
26
243
  .option('-o, --output <path>', 'Output directory')
27
- .option('-f, --format <format>', 'Output format (epub, pdf, both)', 'epub')
28
- .option('-t, --typography <preset>', 'Typography preset (novel, presentation, review, ebook)', 'ebook')
244
+ .option('-f, --format <format>', 'Output format (epub, pdf, both)', 'both')
245
+ .option('-t, --typography <preset>', 'Typography preset (auto, novel, presentation, review, ebook, text_heavy, table_heavy, image_heavy, balanced, report, manual, magazine)', 'auto')
29
246
  .option('-c, --cover <theme>', 'Cover theme')
30
247
  .option('--no-validate', 'Skip content validation')
31
248
  .option('--no-auto-fix', 'Disable auto-fix')
32
249
  .option('--toc-depth <number>', 'Table of contents depth', '2')
33
250
  .option('--no-toc', 'Disable table of contents')
34
- .option('--pdf-engine <engine>', 'PDF engine (pdflatex, xelatex, weasyprint)', 'weasyprint')
251
+ .option('--pdf-engine <engine>', 'PDF engine (auto, pdflatex, xelatex, weasyprint)', 'auto')
35
252
  .option('--paper-size <size>', 'Paper size (a4, letter)', 'a4')
36
253
  .option('--font-subsetting', 'Enable font subsetting')
37
254
  .option('--css <path>', 'Custom CSS file path')
@@ -55,6 +272,15 @@ program
55
272
  if (!inputPath.endsWith('.md')) {
56
273
  console.error(chalk.yellow('⚠️ Warning: Input file does not have .md extension'));
57
274
  }
275
+ const fileContent = fs.readFileSync(inputPath, 'utf-8');
276
+ const analysisResult = analyzeMarkdownContent(fileContent);
277
+ const metadata = extractMetadata(fileContent);
278
+ const inferredTitle = metadata.title || path.basename(inputPath, '.md');
279
+ const inferredAuthor = metadata.author || '';
280
+ const customTitle = (options.title || inferredTitle).trim();
281
+ const customAuthor = (options.author || inferredAuthor).trim();
282
+ const typographyOption = String(options.typography || 'auto');
283
+ const typographyPreset = typographyOption === 'auto' ? analysisResult.recommendedPreset : typographyOption;
58
284
  console.log(chalk.cyan.bold('\n📚 Markdown to Document CLI\n'));
59
285
  // Initialize converter
60
286
  const spinner = ora('Initializing...').start();
@@ -72,7 +298,7 @@ program
72
298
  inputPath,
73
299
  outputPath: options.output ? path.resolve(options.output) : undefined,
74
300
  format: options.format,
75
- typographyPreset: options.typography,
301
+ typographyPreset: typographyPreset,
76
302
  coverTheme: options.cover,
77
303
  validateContent: options.validate !== false,
78
304
  autoFix: options.autoFix !== false,
@@ -82,12 +308,17 @@ program
82
308
  paperSize: options.paperSize,
83
309
  enableFontSubsetting: options.fontSubsetting,
84
310
  cssPath: options.css ? path.resolve(options.css) : undefined,
311
+ customTitle,
312
+ customAuthor: customAuthor || undefined,
85
313
  };
86
314
  // Show conversion info
87
315
  console.log(chalk.gray('─'.repeat(50)));
88
316
  console.log(chalk.bold('📄 Input:'), chalk.cyan(inputPath));
89
317
  console.log(chalk.bold('📤 Format:'), chalk.cyan(conversionOptions.format.toUpperCase()));
90
318
  console.log(chalk.bold('🎨 Typography:'), chalk.cyan(TYPOGRAPHY_PRESETS[conversionOptions.typographyPreset]?.name || conversionOptions.typographyPreset));
319
+ console.log(chalk.bold('📖 Title:'), chalk.cyan(customTitle));
320
+ if (customAuthor)
321
+ console.log(chalk.bold('✍️ Author:'), chalk.cyan(customAuthor));
91
322
  if (conversionOptions.coverTheme) {
92
323
  console.log(chalk.bold('🖼️ Cover:'), chalk.cyan(COVER_THEMES[conversionOptions.coverTheme]?.name || conversionOptions.coverTheme));
93
324
  }
@@ -154,154 +385,296 @@ program
154
385
  process.exit(1);
155
386
  }
156
387
  });
157
- // Interactive mode
388
+ /**
389
+ * Extract metadata from frontmatter
390
+ */
391
+ function extractMetadata(content) {
392
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
393
+ if (!frontmatterMatch)
394
+ return {};
395
+ const frontmatter = frontmatterMatch[1];
396
+ const titleMatch = frontmatter.match(/^title:\s*["']?(.+?)["']?\s*$/m);
397
+ const authorMatch = frontmatter.match(/^author:\s*["']?(.+?)["']?\s*$/m);
398
+ return {
399
+ title: titleMatch?.[1]?.trim(),
400
+ author: authorMatch?.[1]?.trim(),
401
+ };
402
+ }
403
+ /**
404
+ * Get simplified preset choices (top 6 most useful)
405
+ */
406
+ function getSimplifiedPresetChoices(recommendedPreset) {
407
+ const topPresets = ['ebook', 'novel', 'report', 'presentation', 'table_heavy', 'image_heavy'];
408
+ return topPresets.map(presetId => {
409
+ const preset = TYPOGRAPHY_PRESETS[presetId];
410
+ if (!preset)
411
+ return null;
412
+ const isRecommended = presetId === recommendedPreset;
413
+ const name = isRecommended
414
+ ? chalk.green(`★ ${preset.name}`) + chalk.gray(` - ${preset.description}`)
415
+ : chalk.cyan(preset.name) + chalk.gray(` - ${preset.description}`);
416
+ return { name, value: presetId };
417
+ }).filter(Boolean);
418
+ }
419
+ /**
420
+ * Get simplified cover theme choices (top 6)
421
+ */
422
+ function getSimplifiedThemeChoices() {
423
+ const topThemes = ['apple', 'modern_gradient', 'academic', 'corporate', 'minimalist', 'classic_book'];
424
+ return topThemes.map(themeId => {
425
+ const theme = COVER_THEMES[themeId];
426
+ if (!theme)
427
+ return null;
428
+ return {
429
+ name: chalk.cyan(theme.name) + chalk.gray(` - ${theme.description}`),
430
+ value: themeId,
431
+ };
432
+ }).filter(Boolean);
433
+ }
434
+ // Interactive mode - Refactored for better UX
158
435
  program
159
436
  .command('interactive')
160
437
  .alias('i')
161
- .description('Interactive mode with guided prompts')
438
+ .description('Interactive mode with streamlined workflow')
162
439
  .action(async () => {
163
- console.log(chalk.cyan.bold('\n' + ''.repeat(60)));
164
- console.log(chalk.cyan.bold(' Markdown to Document - Interactive Mode'));
165
- console.log(chalk.cyan.bold(''.repeat(60) + '\n'));
166
- const answers = await inquirer.prompt([
440
+ console.log(chalk.cyan.bold('\n' + ''.repeat(60)));
441
+ console.log(chalk.cyan.bold(' 📚 Markdown to Document - Interactive Mode'));
442
+ console.log(chalk.cyan.bold(''.repeat(60) + '\n'));
443
+ // ============ STEP 1: 파일 선택 ============
444
+ console.log(chalk.gray(' Step 1/3: 파일 선택\n'));
445
+ const fileAnswer = await inquirer.prompt([
167
446
  {
168
447
  type: 'input',
169
448
  name: 'inputPath',
170
- message: chalk.yellow('📄 Input markdown file path:'),
449
+ message: chalk.yellow('📄 마크다운 파일 경로:'),
171
450
  validate: (input) => {
172
- // 자동으로 따옴표 제거
173
451
  const cleanedInput = input.trim().replace(/^['"]|['"]$/g, '');
452
+ if (!cleanedInput)
453
+ return chalk.red('파일 경로를 입력하세요.');
174
454
  const resolvedPath = path.resolve(cleanedInput);
175
455
  if (!fs.existsSync(resolvedPath)) {
176
- return chalk.red(' File not found. Please enter a valid path.');
456
+ return chalk.red('파일을 찾을 없습니다.');
457
+ }
458
+ if (!resolvedPath.endsWith('.md')) {
459
+ return chalk.yellow('마크다운 파일(.md)을 선택하세요.');
177
460
  }
178
461
  return true;
179
462
  },
180
- transformer: (input) => {
181
- // 입력값 표시 시에도 따옴표 제거
182
- return input.trim().replace(/^['"]|['"]$/g, '');
183
- },
463
+ transformer: (input) => input.trim().replace(/^['"]|['"]$/g, ''),
184
464
  },
465
+ ]);
466
+ const cleanedInputPath = fileAnswer.inputPath.trim().replace(/^['"]|['"]$/g, '');
467
+ const resolvedInputPath = path.resolve(cleanedInputPath);
468
+ const fileContent = fs.readFileSync(resolvedInputPath, 'utf-8');
469
+ // 문서 분석 (자동)
470
+ const analysisResult = analyzeMarkdownContent(fileContent);
471
+ const metadata = extractMetadata(fileContent);
472
+ // 제목/저자: 반드시 사용자 입력을 받으며, 입력값을 항상 변환에 반영
473
+ const metaAnswers = await inquirer.prompt([
185
474
  {
186
475
  type: 'input',
187
476
  name: 'customTitle',
188
- message: chalk.yellow('📖 Book title (leave empty to use auto-detected):'),
189
- default: '',
477
+ message: chalk.yellow('📖 제목 (Enter=자동):'),
478
+ default: metadata.title || path.basename(resolvedInputPath, '.md'),
479
+ validate: () => true,
480
+ transformer: (input) => input,
190
481
  },
191
482
  {
192
483
  type: 'input',
193
484
  name: 'customAuthor',
194
- message: chalk.yellow('✍️ Author name (leave empty to use auto-detected):'),
195
- default: '',
196
- },
197
- {
198
- type: 'list',
199
- name: 'format',
200
- message: chalk.yellow('📤 Output format:'),
201
- choices: [
202
- { name: chalk.magenta('📚 Both EPUB and PDF'), value: 'both' },
203
- { name: chalk.blue('📄 PDF only'), value: 'pdf' },
204
- { name: chalk.green('📖 EPUB only'), value: 'epub' },
205
- ],
206
- default: 'both',
207
- },
208
- {
209
- type: 'list',
210
- name: 'typographyPreset',
211
- message: chalk.yellow('🎨 Typography preset:'),
212
- choices: Object.values(TYPOGRAPHY_PRESETS).map(preset => ({
213
- name: `${chalk.cyan(preset.name)} - ${chalk.gray(preset.description)}`,
214
- value: preset.id,
215
- })),
216
- default: 'ebook',
485
+ message: chalk.yellow('✍️ 저자 (Enter=자동):'),
486
+ default: metadata.author || '',
487
+ validate: () => true,
488
+ transformer: (input) => input,
217
489
  },
490
+ ]);
491
+ // ============ STEP 2: 모드 선택 및 설정 ============
492
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
493
+ console.log(chalk.gray(' Step 2/3: 변환 설정\n'));
494
+ // 분석 결과 요약 (간략하게)
495
+ console.log(chalk.bold('📊 문서 분석:'));
496
+ const statsLine = [
497
+ `${analysisResult.wordCount.toLocaleString()}단어`,
498
+ `이미지 ${analysisResult.imageCount}개`,
499
+ `표 ${analysisResult.tableCount}개`,
500
+ ].join(' | ');
501
+ console.log(chalk.gray(` ${statsLine}`));
502
+ if (analysisResult.issues.length > 0) {
503
+ console.log(chalk.yellow(` ⚠️ ${analysisResult.issues.length}개 이슈 감지 → 자동 최적화 적용됨`));
504
+ }
505
+ else {
506
+ console.log(chalk.green(' ✅ 표준 Markdown - 바로 변환 가능'));
507
+ }
508
+ if (metadata.title) {
509
+ console.log(chalk.gray(` 📖 제목: ${metadata.title}`));
510
+ }
511
+ console.log();
512
+ // 모드 선택
513
+ const modeAnswer = await inquirer.prompt([
218
514
  {
219
515
  type: 'list',
220
- name: 'coverTheme',
221
- message: chalk.yellow('🖼️ Cover theme (optional):'),
516
+ name: 'mode',
517
+ message: chalk.yellow('🚀 변환 모드 선택:'),
222
518
  choices: [
223
- { name: chalk.gray('None'), value: null },
224
- ...Object.values(COVER_THEMES).map(theme => ({
225
- name: `${chalk.cyan(theme.name)} - ${chalk.gray(theme.description)}`,
226
- value: theme.id,
227
- })),
519
+ {
520
+ name: chalk.green('⚡ 빠른 변환') + chalk.gray(' - 스마트 기본값으로 바로 변환 (권장)'),
521
+ value: 'quick',
522
+ },
523
+ {
524
+ name: chalk.blue('⚙️ 상세 설정') + chalk.gray(' - 모든 옵션을 직접 선택'),
525
+ value: 'custom',
526
+ },
228
527
  ],
229
- default: null,
230
- },
231
- {
232
- type: 'confirm',
233
- name: 'validateContent',
234
- message: chalk.yellow('🔍 Enable content validation?'),
235
- default: true,
236
- },
237
- {
238
- type: 'confirm',
239
- name: 'autoFix',
240
- message: chalk.yellow('🔧 Enable auto-fix for detected issues?'),
241
- default: true,
242
- },
243
- {
244
- type: 'input',
245
- name: 'outputPath',
246
- message: chalk.yellow('📁 Output directory (leave empty for same as input):'),
247
- default: '',
528
+ default: 'quick',
248
529
  },
249
530
  ]);
531
+ const mode = modeAnswer.mode;
532
+ // 변환 설정 수집
533
+ let format = 'both';
534
+ let typographyPreset = analysisResult.recommendedPreset;
535
+ let coverTheme = 'apple';
536
+ const inferredTitle = metadata.title || path.basename(resolvedInputPath, '.md');
537
+ const inferredAuthor = metadata.author || '';
538
+ let customTitle = metaAnswers.customTitle.trim() || inferredTitle;
539
+ let customAuthor = metaAnswers.customAuthor.trim() || inferredAuthor;
540
+ let outputPath = '';
541
+ if (mode === 'quick') {
542
+ // 빠른 모드: 출력 형식만 선택
543
+ const quickAnswers = await inquirer.prompt([
544
+ {
545
+ type: 'list',
546
+ name: 'format',
547
+ message: chalk.yellow('📤 출력 형식:'),
548
+ choices: [
549
+ { name: chalk.magenta('📚 EPUB + PDF'), value: 'both' },
550
+ { name: chalk.green('📖 EPUB만'), value: 'epub' },
551
+ { name: chalk.blue('📄 PDF만'), value: 'pdf' },
552
+ ],
553
+ default: 'both',
554
+ },
555
+ ]);
556
+ format = quickAnswers.format;
557
+ // 스마트 기본값 적용
558
+ console.log(chalk.gray('\n 📋 적용될 설정:'));
559
+ console.log(chalk.gray(` 프리셋: ${TYPOGRAPHY_PRESETS[typographyPreset]?.name || typographyPreset}`));
560
+ console.log(chalk.gray(` 표지: ${COVER_THEMES[coverTheme]?.name || coverTheme}`));
561
+ if (analysisResult.recommendPreprocess) {
562
+ console.log(chalk.gray(' Obsidian 최적화: 자동 적용'));
563
+ }
564
+ }
565
+ else if (mode === 'custom') {
566
+ // 상세 모드: 모든 옵션 선택
567
+ const customAnswers = await inquirer.prompt([
568
+ {
569
+ type: 'list',
570
+ name: 'format',
571
+ message: chalk.yellow('📤 출력 형식:'),
572
+ choices: [
573
+ { name: chalk.magenta('📚 EPUB + PDF'), value: 'both' },
574
+ { name: chalk.green('📖 EPUB만'), value: 'epub' },
575
+ { name: chalk.blue('📄 PDF만'), value: 'pdf' },
576
+ ],
577
+ default: 'both',
578
+ },
579
+ {
580
+ type: 'list',
581
+ name: 'typographyPreset',
582
+ message: chalk.yellow('🎨 타이포그래피 프리셋:'),
583
+ choices: [
584
+ ...getSimplifiedPresetChoices(analysisResult.recommendedPreset),
585
+ new inquirer.Separator(),
586
+ { name: chalk.gray('더 많은 프리셋 보기...'), value: '_more' },
587
+ ],
588
+ default: analysisResult.recommendedPreset,
589
+ },
590
+ {
591
+ type: 'list',
592
+ name: 'coverTheme',
593
+ message: chalk.yellow('🖼️ 표지 테마:'),
594
+ choices: [
595
+ ...getSimplifiedThemeChoices(),
596
+ new inquirer.Separator(),
597
+ { name: chalk.gray('더 많은 테마 보기...'), value: '_more' },
598
+ ],
599
+ default: 'apple',
600
+ },
601
+ ]);
602
+ format = customAnswers.format;
603
+ typographyPreset = customAnswers.typographyPreset;
604
+ coverTheme = customAnswers.coverTheme;
605
+ // "더 보기" 선택 시 전체 목록 표시
606
+ if (typographyPreset === '_more') {
607
+ const morePresetAnswer = await inquirer.prompt([
608
+ {
609
+ type: 'list',
610
+ name: 'typographyPreset',
611
+ message: chalk.yellow('🎨 타이포그래피 프리셋 (전체):'),
612
+ choices: getTypographyPresetChoices(analysisResult),
613
+ default: analysisResult.recommendedPreset,
614
+ },
615
+ ]);
616
+ typographyPreset = morePresetAnswer.typographyPreset;
617
+ }
618
+ if (coverTheme === '_more') {
619
+ const moreThemeAnswer = await inquirer.prompt([
620
+ {
621
+ type: 'list',
622
+ name: 'coverTheme',
623
+ message: chalk.yellow('🖼️ 표지 테마 (전체):'),
624
+ choices: getCoverThemeChoices(),
625
+ default: 'apple',
626
+ },
627
+ ]);
628
+ coverTheme = moreThemeAnswer.coverTheme;
629
+ }
630
+ }
631
+ // ============ STEP 3: 변환 실행 ============
632
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
633
+ console.log(chalk.gray(' Step 3/3: 변환 실행\n'));
250
634
  try {
251
- console.log(chalk.gray('\n' + '─'.repeat(60) + '\n'));
252
- const spinner = ora({
253
- text: chalk.cyan('⚙️ Initializing...'),
254
- spinner: 'dots',
255
- }).start();
635
+ const spinner = ora(chalk.cyan('⚙️ 초기화 중...')).start();
256
636
  const converter = new MarkdownToDocument();
257
637
  const initResult = await converter.initialize();
258
638
  if (!initResult.success) {
259
- spinner.fail(chalk.red(' Initialization failed'));
260
- console.error(chalk.red(`❌ ${initResult.error}`));
639
+ spinner.fail(chalk.red('초기화 실패'));
640
+ console.error(chalk.red(`\n❌ ${initResult.error}`));
261
641
  console.log(chalk.yellow('\n' + MarkdownToDocument.getInstallInstructions()));
262
642
  process.exit(1);
263
643
  }
264
- spinner.succeed(chalk.green('✅ Initialized successfully'));
265
- // 따옴표 제거 경로 해결
266
- const cleanedInputPath = answers.inputPath.trim().replace(/^['"]|['"]$/g, '');
644
+ // 변환 실행
645
+ spinner.text = chalk.cyan('🔄 문서 변환 중...');
267
646
  const conversionOptions = {
268
- inputPath: path.resolve(cleanedInputPath),
269
- outputPath: answers.outputPath ? path.resolve(answers.outputPath) : undefined,
270
- format: answers.format,
271
- typographyPreset: answers.typographyPreset,
272
- coverTheme: answers.coverTheme,
273
- validateContent: answers.validateContent,
274
- autoFix: answers.autoFix,
647
+ inputPath: resolvedInputPath,
648
+ outputPath: outputPath ? path.resolve(outputPath) : undefined,
649
+ format: format,
650
+ typographyPreset: typographyPreset,
651
+ coverTheme: coverTheme,
652
+ validateContent: true,
653
+ autoFix: true,
275
654
  tocDepth: 2,
276
655
  includeToc: true,
277
- customTitle: answers.customTitle || undefined,
278
- customAuthor: answers.customAuthor || undefined,
656
+ customTitle: customTitle || undefined,
657
+ customAuthor: customAuthor || undefined,
279
658
  };
280
- const convertSpinner = ora({
281
- text: chalk.cyan('🔄 Converting document...'),
282
- spinner: 'dots',
283
- }).start();
284
659
  const result = await converter.convert(conversionOptions);
285
660
  if (result.success) {
286
- convertSpinner.succeed(chalk.green(' Conversion completed!'));
287
- console.log(chalk.gray('\n' + '─'.repeat(60)));
288
- console.log(chalk.green.bold('\n📦 Output Files:\n'));
661
+ spinner.succeed(chalk.green('변환 완료!'));
662
+ console.log(chalk.green.bold('\n📦 생성된 파일:\n'));
289
663
  if (result.epubPath) {
290
- console.log(chalk.green(` 📖 EPUB: ${result.epubPath}`));
664
+ console.log(chalk.green(` 📖 ${result.epubPath}`));
291
665
  }
292
666
  if (result.pdfPath) {
293
- console.log(chalk.blue(` 📄 PDF: ${result.pdfPath}`));
667
+ console.log(chalk.blue(` 📄 ${result.pdfPath}`));
294
668
  }
295
- console.log(chalk.gray('\n' + ''.repeat(60)));
296
- console.log(chalk.green.bold('\n🎉 Conversion successful!\n'));
669
+ console.log(chalk.gray('\n' + ''.repeat(60)));
670
+ console.log(chalk.green.bold('🎉 변환이 완료되었습니다!\n'));
297
671
  }
298
672
  else {
299
- convertSpinner.fail(chalk.red(' Conversion failed'));
300
- console.log(chalk.red('\n❌ Errors:'));
673
+ spinner.fail(chalk.red('변환 실패'));
674
+ console.log(chalk.red('\n❌ 오류:'));
301
675
  result.errors.forEach(error => {
302
- console.log(chalk.red(` • ${error}`));
676
+ console.log(chalk.red(` • ${error}`));
303
677
  });
304
- console.log(chalk.red('\n❌ Conversion failed!\n'));
305
678
  process.exit(1);
306
679
  }
307
680
  }