nconv-cli 1.0.0 → 1.0.2
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/.claude/settings.local.json +7 -1
- package/GITHUB_ABOUT.md +53 -0
- package/README.md +285 -131
- package/dist/index.js +1036 -40
- package/dist/index.js.map +1 -1
- package/nconv-cli-1.0.0.tgz +0 -0
- package/nconv-cli-1.0.2.tgz +0 -0
- package/package.json +22 -9
- package/patches/notion-exporter+0.8.1.patch +32 -0
- package/src/commands/html.ts +135 -0
- package/src/commands/init.ts +55 -0
- package/src/commands/md.ts +0 -4
- package/src/commands/pdf.ts +150 -0
- package/src/config.ts +105 -13
- package/src/core/exporter.ts +159 -2
- package/src/index.ts +52 -3
- package/src/repl/commands.ts +535 -0
- package/src/repl/index.ts +87 -0
- package/src/repl/prompts.ts +123 -0
- package/src/utils/logger.ts +5 -5
- package/test/test-export-types.ts +54 -0
- package/test/test-markdown.ts +45 -0
- package/test/test-pdf.ts +44 -0
- package/test-output/drf-serializer/drf-serializer.pdf +0 -0
- package/test-output/drf-serializer/images/Untitled-1.png +0 -0
- package/test-output/drf-serializer/images/Untitled.png +0 -0
- package/README.en.md +0 -200
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ConverterOptions, createConfig } from '../config.js';
|
|
2
|
+
import { NotionMarkdownExporter } from '../core/exporter.js';
|
|
3
|
+
import { generateSafeFilename, extractTitleFromUrl } from '../utils/file.js';
|
|
4
|
+
import * as logger from '../utils/logger.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* pdf 명령어 핸들러
|
|
11
|
+
* Notion → Markdown → PDF 플로우로 깔끔한 PDF 생성
|
|
12
|
+
*/
|
|
13
|
+
export async function pdfCommand(notionUrl: string, options: ConverterOptions) {
|
|
14
|
+
const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// 설정 생성
|
|
18
|
+
const config = createConfig(options);
|
|
19
|
+
|
|
20
|
+
if (config.verbose) {
|
|
21
|
+
logger.info('Configuration loaded successfully');
|
|
22
|
+
console.log(` Output directory: ${config.output}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 1. Notion에서 Markdown 가져오기
|
|
26
|
+
const spinner = logger.spinner('Fetching Notion page as Markdown...');
|
|
27
|
+
|
|
28
|
+
const exporter = new NotionMarkdownExporter({
|
|
29
|
+
tokenV2: config.tokenV2,
|
|
30
|
+
fileToken: config.fileToken,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await exporter.exportWithImages(notionUrl, tempDir);
|
|
36
|
+
spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
spinner.fail('Failed to fetch Notion page');
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. 파일명/폴더명 생성
|
|
43
|
+
let baseFilename: string;
|
|
44
|
+
if (config.filename) {
|
|
45
|
+
baseFilename = config.filename.replace(/\.pdf$/, '');
|
|
46
|
+
} else {
|
|
47
|
+
const title = extractTitleFromUrl(notionUrl);
|
|
48
|
+
baseFilename = generateSafeFilename(title, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. 제목별 폴더 생성 (output/제목/)
|
|
52
|
+
const pageDir = path.join(config.output, baseFilename);
|
|
53
|
+
await fs.mkdir(pageDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
// 4. 이미지 폴더 생성 및 이미지 파일 이동
|
|
56
|
+
const imageOutputDir = path.join(pageDir, config.imageDir);
|
|
57
|
+
await fs.mkdir(imageOutputDir, { recursive: true });
|
|
58
|
+
|
|
59
|
+
if (config.verbose && result.imageFiles.length > 0) {
|
|
60
|
+
console.log(`Processing image files...\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let processedMarkdown = result.markdown;
|
|
64
|
+
for (const imageFile of result.imageFiles) {
|
|
65
|
+
try {
|
|
66
|
+
const originalFileName = path.basename(imageFile.filename);
|
|
67
|
+
const safeFileName = originalFileName.replace(/\s+/g, '-');
|
|
68
|
+
|
|
69
|
+
// 이미지 파일 복사
|
|
70
|
+
const targetPath = path.join(imageOutputDir, safeFileName);
|
|
71
|
+
await fs.copyFile(imageFile.sourcePath, targetPath);
|
|
72
|
+
|
|
73
|
+
if (config.verbose) {
|
|
74
|
+
console.log(`✓ ${safeFileName}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// PDF용: 이미지를 base64로 변환
|
|
78
|
+
const imageBuffer = await fs.readFile(targetPath);
|
|
79
|
+
const base64 = imageBuffer.toString('base64');
|
|
80
|
+
|
|
81
|
+
// 파일 확장자에서 MIME 타입 결정
|
|
82
|
+
const ext = path.extname(safeFileName).slice(1).toLowerCase();
|
|
83
|
+
const mimeType = ext === 'jpg' ? 'jpeg' : ext;
|
|
84
|
+
const dataUrl = `data:image/${mimeType};base64,${base64}`;
|
|
85
|
+
|
|
86
|
+
// Markdown 내 경로를 처리
|
|
87
|
+
const originalPath = imageFile.filename;
|
|
88
|
+
const relativePath = `./${config.imageDir}/${safeFileName}`;
|
|
89
|
+
|
|
90
|
+
const pathParts = originalPath.split('/');
|
|
91
|
+
const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
|
|
92
|
+
|
|
93
|
+
const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
|
|
95
|
+
// PDF용 Markdown: base64 data URL 사용
|
|
96
|
+
processedMarkdown = processedMarkdown
|
|
97
|
+
.replace(new RegExp(`\\(${escapeRegex(originalPath)}\\)`, 'g'), `(${dataUrl})`)
|
|
98
|
+
.replace(new RegExp(`\\(${escapeRegex(encodedPath)}\\)`, 'g'), `(${dataUrl})`);
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (config.verbose) {
|
|
102
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
103
|
+
console.error(`✗ ${imageFile.filename}: ${errorMsg}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 5. Markdown → PDF 변환
|
|
109
|
+
const pdfSpinner = logger.spinner('Converting Markdown to PDF...');
|
|
110
|
+
|
|
111
|
+
const filename = `${baseFilename}.pdf`;
|
|
112
|
+
const pdfPath = path.join(pageDir, filename);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await exporter.exportMarkdownToPDF(processedMarkdown, pdfPath, {
|
|
116
|
+
format: 'A4',
|
|
117
|
+
});
|
|
118
|
+
pdfSpinner.succeed('PDF generated successfully');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
pdfSpinner.fail('Failed to generate PDF');
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 6. 결과 출력
|
|
125
|
+
console.log('');
|
|
126
|
+
logger.success('PDF export complete!');
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);
|
|
129
|
+
console.log(`📄 PDF: ${filename}`);
|
|
130
|
+
if (result.imageFiles.length > 0) {
|
|
131
|
+
console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
logger.error(error.message);
|
|
138
|
+
} else {
|
|
139
|
+
logger.error('An unknown error occurred.');
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
} finally {
|
|
143
|
+
// 임시 디렉토리 정리
|
|
144
|
+
try {
|
|
145
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
146
|
+
} catch {
|
|
147
|
+
// 무시
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { config as dotenvConfig } from 'dotenv';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import * as logger from './utils/logger.js';
|
|
4
6
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
+
// Disable dotenv default logging
|
|
8
|
+
process.env.DOTENV_CONFIG_SILENT = 'true';
|
|
7
9
|
|
|
8
10
|
export interface NotionConfig {
|
|
9
11
|
tokenV2: string;
|
|
@@ -19,21 +21,108 @@ export interface ConverterOptions {
|
|
|
19
21
|
|
|
20
22
|
export interface FullConfig extends NotionConfig, ConverterOptions {}
|
|
21
23
|
|
|
24
|
+
function getConfigPath(): string {
|
|
25
|
+
return join(os.homedir(), '.nconv', '.env');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loads environment variables from the global config file.
|
|
30
|
+
*/
|
|
31
|
+
function loadEnv() {
|
|
32
|
+
const configPath = getConfigPath();
|
|
33
|
+
|
|
34
|
+
if (!existsSync(configPath)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Manually parse .env file to avoid dotenv logging
|
|
40
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
41
|
+
let loadedCount = 0;
|
|
42
|
+
|
|
43
|
+
content.split('\n').forEach((line) => {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
|
|
46
|
+
// Skip comments and empty lines
|
|
47
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse key=value
|
|
52
|
+
const equalIndex = trimmed.indexOf('=');
|
|
53
|
+
if (equalIndex === -1) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
58
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
59
|
+
|
|
60
|
+
if (key && value) {
|
|
61
|
+
process.env[key] = value;
|
|
62
|
+
loadedCount++;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (process.env.NCONV_VERBOSE) {
|
|
67
|
+
logger.info(`✓ Loaded ${loadedCount} environment variable(s) from ${configPath}`);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error(`Failed to load configuration from: ${configPath}`);
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
logger.error(error.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks if the required environment variables are set.
|
|
79
|
+
* Exits the process if tokens are missing (for CLI commands).
|
|
80
|
+
*/
|
|
81
|
+
function checkEnv() {
|
|
82
|
+
const configPath = getConfigPath();
|
|
83
|
+
if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {
|
|
84
|
+
logger.error('Notion tokens are not set.');
|
|
85
|
+
if (!existsSync(configPath)) {
|
|
86
|
+
logger.error('Configuration file not found.');
|
|
87
|
+
logger.error('Please run "nconv init" to create a configuration file.');
|
|
88
|
+
} else {
|
|
89
|
+
logger.error(`Please set TOKEN_V2 and FILE_TOKEN in: ${configPath}`);
|
|
90
|
+
}
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates if the required environment variables are set.
|
|
97
|
+
* Returns true if valid, false otherwise (for REPL mode).
|
|
98
|
+
*/
|
|
99
|
+
export function validateConfig(): { valid: boolean; message?: string } {
|
|
100
|
+
loadEnv();
|
|
101
|
+
const configPath = getConfigPath();
|
|
102
|
+
|
|
103
|
+
if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {
|
|
104
|
+
if (!existsSync(configPath)) {
|
|
105
|
+
return {
|
|
106
|
+
valid: false,
|
|
107
|
+
message: 'Configuration file not found. Please run /init to set up your Notion tokens.',
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return {
|
|
111
|
+
valid: false,
|
|
112
|
+
message: `Notion tokens are not set. Please run /init or /config to set up your tokens.\nConfig file: ${configPath}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { valid: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
22
120
|
/**
|
|
23
121
|
* Notion 인증 설정 가져오기
|
|
24
122
|
*/
|
|
25
123
|
export function getNotionConfig(): NotionConfig {
|
|
26
124
|
const tokenV2 = process.env.TOKEN_V2 || '';
|
|
27
125
|
const fileToken = process.env.FILE_TOKEN || '';
|
|
28
|
-
|
|
29
|
-
if (!tokenV2 || !fileToken) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
'Notion 토큰이 설정되지 않았습니다.\n' +
|
|
32
|
-
'.env 파일에 TOKEN_V2와 FILE_TOKEN을 설정해주세요.\n' +
|
|
33
|
-
'자세한 내용은 .env.example을 참고하세요.'
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
126
|
return { tokenV2, fileToken };
|
|
38
127
|
}
|
|
39
128
|
|
|
@@ -41,6 +130,9 @@ export function getNotionConfig(): NotionConfig {
|
|
|
41
130
|
* 전체 설정 생성
|
|
42
131
|
*/
|
|
43
132
|
export function createConfig(options: ConverterOptions): FullConfig {
|
|
133
|
+
loadEnv();
|
|
134
|
+
checkEnv();
|
|
135
|
+
|
|
44
136
|
const notionConfig = getNotionConfig();
|
|
45
137
|
|
|
46
138
|
// 출력 디렉토리 절대 경로로 변환
|
package/src/core/exporter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NotionExporter } from 'notion-exporter';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { mdToPdf } from 'md-to-pdf';
|
|
4
5
|
import type { NotionConfig } from '../config.js';
|
|
5
6
|
|
|
6
7
|
export interface ExportResult {
|
|
@@ -8,14 +9,37 @@ export interface ExportResult {
|
|
|
8
9
|
imageFiles: Array<{ filename: string; sourcePath: string }>;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export interface HTMLExportResult {
|
|
13
|
+
html: string;
|
|
14
|
+
imageFiles: Array<{ filename: string; sourcePath: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PDFExportResult {
|
|
18
|
+
pdfPath: string;
|
|
19
|
+
filename: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MarkdownPDFOptions {
|
|
23
|
+
format?: 'A4' | 'Letter';
|
|
24
|
+
margin?: {
|
|
25
|
+
top?: string;
|
|
26
|
+
right?: string;
|
|
27
|
+
bottom?: string;
|
|
28
|
+
left?: string;
|
|
29
|
+
};
|
|
30
|
+
stylesheet?: string | string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
/**
|
|
12
34
|
* Notion 페이지를 마크다운으로 내보내기
|
|
13
35
|
*/
|
|
14
36
|
export class NotionMarkdownExporter {
|
|
15
37
|
private exporter: NotionExporter;
|
|
16
38
|
|
|
17
|
-
constructor(config: NotionConfig) {
|
|
18
|
-
this.exporter = new NotionExporter(config.tokenV2, config.fileToken
|
|
39
|
+
constructor(config: NotionConfig, exportType: 'markdown' | 'html' | 'pdf' = 'markdown') {
|
|
40
|
+
this.exporter = new NotionExporter(config.tokenV2, config.fileToken, {
|
|
41
|
+
exportType,
|
|
42
|
+
});
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
/**
|
|
@@ -72,4 +96,137 @@ export class NotionMarkdownExporter {
|
|
|
72
96
|
throw new Error('Failed to fetch Notion page.');
|
|
73
97
|
}
|
|
74
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Notion URL에서 HTML과 이미지 파일 가져오기
|
|
102
|
+
*/
|
|
103
|
+
async exportHTML(notionUrl: string, tempDir: string): Promise<HTMLExportResult> {
|
|
104
|
+
try {
|
|
105
|
+
// 임시 디렉토리 생성
|
|
106
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// getMdFiles로 HTML과 이미지를 함께 다운로드
|
|
109
|
+
await this.exporter.getMdFiles(notionUrl, tempDir);
|
|
110
|
+
|
|
111
|
+
// 다운로드된 파일 목록 가져오기
|
|
112
|
+
const files = await fs.readdir(tempDir, { withFileTypes: true });
|
|
113
|
+
|
|
114
|
+
// HTML 파일 찾기
|
|
115
|
+
const htmlFile = files.find(f => f.isFile() && f.name.endsWith('.html'));
|
|
116
|
+
if (!htmlFile) {
|
|
117
|
+
throw new Error('HTML file not found.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// HTML 내용 읽기
|
|
121
|
+
const htmlPath = path.join(tempDir, htmlFile.name);
|
|
122
|
+
const html = await fs.readFile(htmlPath, 'utf-8');
|
|
123
|
+
|
|
124
|
+
// 이미지 파일 목록 (HTML이 아닌 파일들)
|
|
125
|
+
const imageFiles = files
|
|
126
|
+
.filter(f => f.isFile() && !f.name.endsWith('.html'))
|
|
127
|
+
.map(f => ({
|
|
128
|
+
filename: f.name,
|
|
129
|
+
sourcePath: path.join(tempDir, f.name),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// 디렉토리 내 폴더도 확인 (Notion이 폴더 구조로 저장할 수 있음)
|
|
133
|
+
const dirs = files.filter(f => f.isDirectory());
|
|
134
|
+
for (const dir of dirs) {
|
|
135
|
+
const subFiles = await fs.readdir(path.join(tempDir, dir.name), { withFileTypes: true });
|
|
136
|
+
for (const subFile of subFiles) {
|
|
137
|
+
if (subFile.isFile() && !subFile.name.endsWith('.html')) {
|
|
138
|
+
imageFiles.push({
|
|
139
|
+
filename: path.join(dir.name, subFile.name),
|
|
140
|
+
sourcePath: path.join(tempDir, dir.name, subFile.name),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { html, imageFiles };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof Error) {
|
|
149
|
+
throw new Error(`Failed to fetch Notion page as HTML: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
throw new Error('Failed to fetch Notion page as HTML.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Markdown을 PDF로 변환 (md-to-pdf 사용)
|
|
157
|
+
* GitHub 스타일의 깔끔한 PDF 생성
|
|
158
|
+
*/
|
|
159
|
+
async exportMarkdownToPDF(
|
|
160
|
+
markdownContent: string,
|
|
161
|
+
outputPath: string,
|
|
162
|
+
options: MarkdownPDFOptions = {}
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
try {
|
|
165
|
+
const pdfOptions = {
|
|
166
|
+
content: markdownContent,
|
|
167
|
+
stylesheet: options.stylesheet || [
|
|
168
|
+
'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css',
|
|
169
|
+
],
|
|
170
|
+
body_class: 'markdown-body',
|
|
171
|
+
css: `
|
|
172
|
+
.markdown-body {
|
|
173
|
+
box-sizing: border-box;
|
|
174
|
+
min-width: 200px;
|
|
175
|
+
max-width: 980px;
|
|
176
|
+
margin: 0 auto;
|
|
177
|
+
padding: 45px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (max-width: 767px) {
|
|
181
|
+
.markdown-body {
|
|
182
|
+
padding: 15px;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* 코드 블록 스타일 개선 */
|
|
187
|
+
.markdown-body pre {
|
|
188
|
+
background-color: #f6f8fa;
|
|
189
|
+
border-radius: 6px;
|
|
190
|
+
padding: 16px;
|
|
191
|
+
overflow: auto;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.markdown-body code {
|
|
195
|
+
background-color: rgba(175, 184, 193, 0.2);
|
|
196
|
+
border-radius: 6px;
|
|
197
|
+
padding: 0.2em 0.4em;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* 이미지 스타일 */
|
|
201
|
+
.markdown-body img {
|
|
202
|
+
max-width: 100%;
|
|
203
|
+
height: auto;
|
|
204
|
+
}
|
|
205
|
+
`,
|
|
206
|
+
pdf_options: {
|
|
207
|
+
format: options.format || 'A4',
|
|
208
|
+
margin: options.margin || {
|
|
209
|
+
top: '20mm',
|
|
210
|
+
right: '20mm',
|
|
211
|
+
bottom: '20mm',
|
|
212
|
+
left: '20mm',
|
|
213
|
+
},
|
|
214
|
+
printBackground: true,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const result = await mdToPdf(pdfOptions);
|
|
219
|
+
|
|
220
|
+
if (result.content) {
|
|
221
|
+
await fs.writeFile(outputPath, result.content);
|
|
222
|
+
} else {
|
|
223
|
+
throw new Error('PDF content is empty');
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error instanceof Error) {
|
|
227
|
+
throw new Error(`Failed to convert Markdown to PDF: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
throw new Error('Failed to convert Markdown to PDF.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
75
232
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { mdCommand } from './commands/md.js';
|
|
5
|
+
import { htmlCommand } from './commands/html.js';
|
|
6
|
+
import { pdfCommand } from './commands/pdf.js';
|
|
5
7
|
import { debugCommand } from './commands/debug.js';
|
|
8
|
+
import { handler as initHandler } from './commands/init.js';
|
|
9
|
+
import { startRepl } from './repl/index.js';
|
|
6
10
|
|
|
7
11
|
const program = new Command();
|
|
8
12
|
|
|
@@ -11,10 +15,17 @@ program
|
|
|
11
15
|
.description('CLI tool for converting Notion pages to blog-ready markdown')
|
|
12
16
|
.version('1.0.0');
|
|
13
17
|
|
|
18
|
+
program
|
|
19
|
+
.command('init')
|
|
20
|
+
.description('Create a default .env configuration file')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
await initHandler({} as any); // Handler expects argv, but doesn't use it
|
|
23
|
+
});
|
|
24
|
+
|
|
14
25
|
program
|
|
15
26
|
.command('md <url>')
|
|
16
27
|
.description('Convert a Notion page to markdown')
|
|
17
|
-
.option('-o, --output <dir>', 'Output directory', './output')
|
|
28
|
+
.option('-o, --output <dir>', 'Output directory', './nconv-output')
|
|
18
29
|
.option('-i, --image-dir <dir>', 'Image folder name (relative to output)', 'images')
|
|
19
30
|
.option('-f, --filename <name>', '출력 파일명 (확장자 제외 또는 포함)')
|
|
20
31
|
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
@@ -27,12 +38,43 @@ program
|
|
|
27
38
|
});
|
|
28
39
|
});
|
|
29
40
|
|
|
41
|
+
program
|
|
42
|
+
.command('html <url>')
|
|
43
|
+
.description('Convert a Notion page to HTML')
|
|
44
|
+
.option('-o, --output <dir>', 'Output directory', './nconv-output')
|
|
45
|
+
.option('-i, --image-dir <dir>', 'Image folder name (relative to output)', 'images')
|
|
46
|
+
.option('-f, --filename <name>', 'Output filename (without extension or with)')
|
|
47
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
48
|
+
.action(async (url: string, options) => {
|
|
49
|
+
await htmlCommand(url, {
|
|
50
|
+
output: options.output,
|
|
51
|
+
imageDir: options.imageDir,
|
|
52
|
+
filename: options.filename,
|
|
53
|
+
verbose: options.verbose,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('pdf <url>')
|
|
59
|
+
.description('Convert a Notion page to PDF (renders actual Notion page)')
|
|
60
|
+
.option('-o, --output <dir>', 'Output directory', './nconv-output')
|
|
61
|
+
.option('-f, --filename <name>', 'Output filename (without extension or with)')
|
|
62
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
63
|
+
.action(async (url: string, options) => {
|
|
64
|
+
await pdfCommand(url, {
|
|
65
|
+
output: options.output,
|
|
66
|
+
imageDir: 'images', // Not used for PDF, but required by interface
|
|
67
|
+
filename: options.filename,
|
|
68
|
+
verbose: options.verbose,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
30
72
|
// 개발 환경에서만 debug 명령어를 활성화
|
|
31
73
|
if (process.env.NODE_ENV !== 'production') {
|
|
32
74
|
program
|
|
33
75
|
.command('debug <url>')
|
|
34
76
|
.description('Debug: Output raw markdown and image URLs')
|
|
35
|
-
.option('-o, --output <dir>', '출력 디렉토리', './output')
|
|
77
|
+
.option('-o, --output <dir>', '출력 디렉토리', './nconv-output')
|
|
36
78
|
.option('-i, --image-dir <dir>', 'Image folder name', 'images')
|
|
37
79
|
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
38
80
|
.action(async (url: string, options) => {
|
|
@@ -45,4 +87,11 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
45
87
|
});
|
|
46
88
|
}
|
|
47
89
|
|
|
48
|
-
|
|
90
|
+
// If no arguments provided, start REPL mode
|
|
91
|
+
(async () => {
|
|
92
|
+
if (process.argv.length === 2) {
|
|
93
|
+
await startRepl();
|
|
94
|
+
} else {
|
|
95
|
+
program.parse();
|
|
96
|
+
}
|
|
97
|
+
})();
|