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.
- package/README.md +54 -9
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +476 -103
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/services/CoverService.d.ts +25 -3
- package/dist/services/CoverService.d.ts.map +1 -1
- package/dist/services/CoverService.js +226 -97
- package/dist/services/CoverService.js.map +1 -1
- package/dist/services/MarkdownPreprocessor.d.ts +5 -0
- package/dist/services/MarkdownPreprocessor.d.ts.map +1 -1
- package/dist/services/MarkdownPreprocessor.js +18 -7
- package/dist/services/MarkdownPreprocessor.js.map +1 -1
- package/dist/services/PandocService.d.ts +3 -1
- package/dist/services/PandocService.d.ts.map +1 -1
- package/dist/services/PandocService.js +31 -15
- package/dist/services/PandocService.js.map +1 -1
- package/dist/services/TypographyService.d.ts +8 -1
- package/dist/services/TypographyService.d.ts.map +1 -1
- package/dist/services/TypographyService.js +417 -57
- package/dist/services/TypographyService.js.map +1 -1
- package/dist/types/index.d.ts +37 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +28 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +224 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/cssBuilder.d.ts +93 -0
- package/dist/utils/cssBuilder.d.ts.map +1 -0
- package/dist/utils/cssBuilder.js +231 -0
- package/dist/utils/cssBuilder.js.map +1 -0
- package/dist/utils/themeUtils.d.ts +70 -0
- package/dist/utils/themeUtils.d.ts.map +1 -0
- package/dist/utils/themeUtils.js +141 -0
- package/dist/utils/themeUtils.js.map +1 -0
- 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(
|
|
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)', '
|
|
28
|
-
.option('-t, --typography <preset>', 'Typography preset (novel, presentation, review, 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)', '
|
|
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:
|
|
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
|
-
|
|
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
|
|
438
|
+
.description('Interactive mode with streamlined workflow')
|
|
162
439
|
.action(async () => {
|
|
163
|
-
console.log(chalk.cyan.bold('\n' + '
|
|
164
|
-
console.log(chalk.cyan.bold(' Markdown to Document - Interactive Mode'));
|
|
165
|
-
console.log(chalk.cyan.bold('
|
|
166
|
-
|
|
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('📄
|
|
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('
|
|
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('📖
|
|
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('✍️
|
|
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: '
|
|
221
|
-
message: chalk.yellow('
|
|
516
|
+
name: 'mode',
|
|
517
|
+
message: chalk.yellow('🚀 변환 모드 선택:'),
|
|
222
518
|
choices: [
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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:
|
|
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
|
-
|
|
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('
|
|
260
|
-
console.error(chalk.red(
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
const cleanedInputPath = answers.inputPath.trim().replace(/^['"]|['"]$/g, '');
|
|
644
|
+
// 변환 실행
|
|
645
|
+
spinner.text = chalk.cyan('🔄 문서 변환 중...');
|
|
267
646
|
const conversionOptions = {
|
|
268
|
-
inputPath:
|
|
269
|
-
outputPath:
|
|
270
|
-
format:
|
|
271
|
-
typographyPreset:
|
|
272
|
-
coverTheme:
|
|
273
|
-
validateContent:
|
|
274
|
-
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:
|
|
278
|
-
customAuthor:
|
|
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
|
-
|
|
287
|
-
console.log(chalk.
|
|
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(`
|
|
664
|
+
console.log(chalk.green(` 📖 ${result.epubPath}`));
|
|
291
665
|
}
|
|
292
666
|
if (result.pdfPath) {
|
|
293
|
-
console.log(chalk.blue(`
|
|
667
|
+
console.log(chalk.blue(` 📄 ${result.pdfPath}`));
|
|
294
668
|
}
|
|
295
|
-
console.log(chalk.gray('\n' + '
|
|
296
|
-
console.log(chalk.green.bold('
|
|
669
|
+
console.log(chalk.gray('\n' + '═'.repeat(60)));
|
|
670
|
+
console.log(chalk.green.bold('🎉 변환이 완료되었습니다!\n'));
|
|
297
671
|
}
|
|
298
672
|
else {
|
|
299
|
-
|
|
300
|
-
console.log(chalk.red('\n❌
|
|
673
|
+
spinner.fail(chalk.red('변환 실패'));
|
|
674
|
+
console.log(chalk.red('\n❌ 오류:'));
|
|
301
675
|
result.errors.forEach(error => {
|
|
302
|
-
console.log(chalk.red(`
|
|
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
|
}
|