markdown-to-document-cli 1.1.4 → 1.2.8

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