nconv-cli 1.0.0 → 1.0.3
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/nconv-cli-1.0.3.tgz +0 -0
- package/package.json +23 -10
- 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
package/dist/index.js
CHANGED
|
@@ -4,20 +4,108 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
|
-
import {
|
|
8
|
-
import { resolve } from "path";
|
|
9
|
-
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { resolve, join } from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
function success(message) {
|
|
15
|
+
console.log(chalk.green("\u2713"), message);
|
|
16
|
+
}
|
|
17
|
+
function error(message) {
|
|
18
|
+
console.error(chalk.red("\u2717"), message);
|
|
19
|
+
}
|
|
20
|
+
function info(message) {
|
|
21
|
+
console.log(chalk.blue("\u2139"), message);
|
|
22
|
+
}
|
|
23
|
+
function warn(message) {
|
|
24
|
+
console.log(chalk.yellow("\u26A0"), message);
|
|
25
|
+
}
|
|
26
|
+
function spinner(text) {
|
|
27
|
+
return ora(text).start();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/config.ts
|
|
31
|
+
process.env.DOTENV_CONFIG_SILENT = "true";
|
|
32
|
+
function getConfigPath() {
|
|
33
|
+
return join(os.homedir(), ".nconv", ".env");
|
|
34
|
+
}
|
|
35
|
+
function loadEnv() {
|
|
36
|
+
const configPath = getConfigPath();
|
|
37
|
+
if (!existsSync(configPath)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(configPath, "utf-8");
|
|
42
|
+
let loadedCount = 0;
|
|
43
|
+
content.split("\n").forEach((line) => {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const equalIndex = trimmed.indexOf("=");
|
|
49
|
+
if (equalIndex === -1) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
53
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
54
|
+
if (key && value) {
|
|
55
|
+
process.env[key] = value;
|
|
56
|
+
loadedCount++;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (process.env.NCONV_VERBOSE) {
|
|
60
|
+
info(`\u2713 Loaded ${loadedCount} environment variable(s) from ${configPath}`);
|
|
61
|
+
}
|
|
62
|
+
} catch (error2) {
|
|
63
|
+
error(`Failed to load configuration from: ${configPath}`);
|
|
64
|
+
if (error2 instanceof Error) {
|
|
65
|
+
error(error2.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function checkEnv() {
|
|
70
|
+
const configPath = getConfigPath();
|
|
71
|
+
if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {
|
|
72
|
+
error("Notion tokens are not set.");
|
|
73
|
+
if (!existsSync(configPath)) {
|
|
74
|
+
error("Configuration file not found.");
|
|
75
|
+
error('Please run "nconv init" to create a configuration file.');
|
|
76
|
+
} else {
|
|
77
|
+
error(`Please set TOKEN_V2 and FILE_TOKEN in: ${configPath}`);
|
|
78
|
+
}
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function validateConfig() {
|
|
83
|
+
loadEnv();
|
|
84
|
+
const configPath = getConfigPath();
|
|
85
|
+
if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {
|
|
86
|
+
if (!existsSync(configPath)) {
|
|
87
|
+
return {
|
|
88
|
+
valid: false,
|
|
89
|
+
message: "Configuration file not found. Please run /init to set up your Notion tokens."
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
return {
|
|
93
|
+
valid: false,
|
|
94
|
+
message: `Notion tokens are not set. Please run /init or /config to set up your tokens.
|
|
95
|
+
Config file: ${configPath}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { valid: true };
|
|
100
|
+
}
|
|
10
101
|
function getNotionConfig() {
|
|
11
102
|
const tokenV2 = process.env.TOKEN_V2 || "";
|
|
12
103
|
const fileToken = process.env.FILE_TOKEN || "";
|
|
13
|
-
if (!tokenV2 || !fileToken) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
"Notion \uD1A0\uD070\uC774 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n.env \uD30C\uC77C\uC5D0 TOKEN_V2\uC640 FILE_TOKEN\uC744 \uC124\uC815\uD574\uC8FC\uC138\uC694.\n\uC790\uC138\uD55C \uB0B4\uC6A9\uC740 .env.example\uC744 \uCC38\uACE0\uD558\uC138\uC694."
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
104
|
return { tokenV2, fileToken };
|
|
19
105
|
}
|
|
20
106
|
function createConfig(options) {
|
|
107
|
+
loadEnv();
|
|
108
|
+
checkEnv();
|
|
21
109
|
const notionConfig = getNotionConfig();
|
|
22
110
|
const outputDir = resolve(process.cwd(), options.output);
|
|
23
111
|
return {
|
|
@@ -31,9 +119,12 @@ function createConfig(options) {
|
|
|
31
119
|
import { NotionExporter } from "notion-exporter";
|
|
32
120
|
import { promises as fs } from "fs";
|
|
33
121
|
import path from "path";
|
|
122
|
+
import { mdToPdf } from "md-to-pdf";
|
|
34
123
|
var NotionMarkdownExporter = class {
|
|
35
|
-
constructor(config) {
|
|
36
|
-
this.exporter = new NotionExporter(config.tokenV2, config.fileToken
|
|
124
|
+
constructor(config, exportType = "markdown") {
|
|
125
|
+
this.exporter = new NotionExporter(config.tokenV2, config.fileToken, {
|
|
126
|
+
exportType
|
|
127
|
+
});
|
|
37
128
|
}
|
|
38
129
|
/**
|
|
39
130
|
* Notion URL에서 마크다운과 이미지 파일 가져오기
|
|
@@ -73,6 +164,115 @@ var NotionMarkdownExporter = class {
|
|
|
73
164
|
throw new Error("Failed to fetch Notion page.");
|
|
74
165
|
}
|
|
75
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Notion URL에서 HTML과 이미지 파일 가져오기
|
|
169
|
+
*/
|
|
170
|
+
async exportHTML(notionUrl, tempDir) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
173
|
+
await this.exporter.getMdFiles(notionUrl, tempDir);
|
|
174
|
+
const files = await fs.readdir(tempDir, { withFileTypes: true });
|
|
175
|
+
const htmlFile = files.find((f) => f.isFile() && f.name.endsWith(".html"));
|
|
176
|
+
if (!htmlFile) {
|
|
177
|
+
throw new Error("HTML file not found.");
|
|
178
|
+
}
|
|
179
|
+
const htmlPath = path.join(tempDir, htmlFile.name);
|
|
180
|
+
const html = await fs.readFile(htmlPath, "utf-8");
|
|
181
|
+
const imageFiles = files.filter((f) => f.isFile() && !f.name.endsWith(".html")).map((f) => ({
|
|
182
|
+
filename: f.name,
|
|
183
|
+
sourcePath: path.join(tempDir, f.name)
|
|
184
|
+
}));
|
|
185
|
+
const dirs = files.filter((f) => f.isDirectory());
|
|
186
|
+
for (const dir of dirs) {
|
|
187
|
+
const subFiles = await fs.readdir(path.join(tempDir, dir.name), { withFileTypes: true });
|
|
188
|
+
for (const subFile of subFiles) {
|
|
189
|
+
if (subFile.isFile() && !subFile.name.endsWith(".html")) {
|
|
190
|
+
imageFiles.push({
|
|
191
|
+
filename: path.join(dir.name, subFile.name),
|
|
192
|
+
sourcePath: path.join(tempDir, dir.name, subFile.name)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { html, imageFiles };
|
|
198
|
+
} catch (error2) {
|
|
199
|
+
if (error2 instanceof Error) {
|
|
200
|
+
throw new Error(`Failed to fetch Notion page as HTML: ${error2.message}`);
|
|
201
|
+
}
|
|
202
|
+
throw new Error("Failed to fetch Notion page as HTML.");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Markdown을 PDF로 변환 (md-to-pdf 사용)
|
|
207
|
+
* GitHub 스타일의 깔끔한 PDF 생성
|
|
208
|
+
*/
|
|
209
|
+
async exportMarkdownToPDF(markdownContent, outputPath, options = {}) {
|
|
210
|
+
try {
|
|
211
|
+
const pdfOptions = {
|
|
212
|
+
content: markdownContent,
|
|
213
|
+
stylesheet: options.stylesheet || [
|
|
214
|
+
"https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"
|
|
215
|
+
],
|
|
216
|
+
body_class: "markdown-body",
|
|
217
|
+
css: `
|
|
218
|
+
.markdown-body {
|
|
219
|
+
box-sizing: border-box;
|
|
220
|
+
min-width: 200px;
|
|
221
|
+
max-width: 980px;
|
|
222
|
+
margin: 0 auto;
|
|
223
|
+
padding: 45px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@media (max-width: 767px) {
|
|
227
|
+
.markdown-body {
|
|
228
|
+
padding: 15px;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* \uCF54\uB4DC \uBE14\uB85D \uC2A4\uD0C0\uC77C \uAC1C\uC120 */
|
|
233
|
+
.markdown-body pre {
|
|
234
|
+
background-color: #f6f8fa;
|
|
235
|
+
border-radius: 6px;
|
|
236
|
+
padding: 16px;
|
|
237
|
+
overflow: auto;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.markdown-body code {
|
|
241
|
+
background-color: rgba(175, 184, 193, 0.2);
|
|
242
|
+
border-radius: 6px;
|
|
243
|
+
padding: 0.2em 0.4em;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* \uC774\uBBF8\uC9C0 \uC2A4\uD0C0\uC77C */
|
|
247
|
+
.markdown-body img {
|
|
248
|
+
max-width: 100%;
|
|
249
|
+
height: auto;
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
pdf_options: {
|
|
253
|
+
format: options.format || "A4",
|
|
254
|
+
margin: options.margin || {
|
|
255
|
+
top: "20mm",
|
|
256
|
+
right: "20mm",
|
|
257
|
+
bottom: "20mm",
|
|
258
|
+
left: "20mm"
|
|
259
|
+
},
|
|
260
|
+
printBackground: true
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const result = await mdToPdf(pdfOptions);
|
|
264
|
+
if (result.content) {
|
|
265
|
+
await fs.writeFile(outputPath, result.content);
|
|
266
|
+
} else {
|
|
267
|
+
throw new Error("PDF content is empty");
|
|
268
|
+
}
|
|
269
|
+
} catch (error2) {
|
|
270
|
+
if (error2 instanceof Error) {
|
|
271
|
+
throw new Error(`Failed to convert Markdown to PDF: ${error2.message}`);
|
|
272
|
+
}
|
|
273
|
+
throw new Error("Failed to convert Markdown to PDF.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
76
276
|
};
|
|
77
277
|
|
|
78
278
|
// src/utils/file.ts
|
|
@@ -115,28 +315,12 @@ async function saveMarkdownFile(outputDir, filename, content) {
|
|
|
115
315
|
return filePath;
|
|
116
316
|
}
|
|
117
317
|
|
|
118
|
-
// src/utils/logger.ts
|
|
119
|
-
import chalk from "chalk";
|
|
120
|
-
import ora from "ora";
|
|
121
|
-
function success(message) {
|
|
122
|
-
console.log(chalk.green("\u2713"), message);
|
|
123
|
-
}
|
|
124
|
-
function error(message) {
|
|
125
|
-
console.error(chalk.red("\u2717"), message);
|
|
126
|
-
}
|
|
127
|
-
function info(message) {
|
|
128
|
-
console.log(chalk.blue("\u2139"), message);
|
|
129
|
-
}
|
|
130
|
-
function spinner(text) {
|
|
131
|
-
return ora(text).start();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
318
|
// src/commands/md.ts
|
|
135
319
|
import path3 from "path";
|
|
136
320
|
import { promises as fs3 } from "fs";
|
|
137
|
-
import
|
|
321
|
+
import os2 from "os";
|
|
138
322
|
async function mdCommand(notionUrl, options) {
|
|
139
|
-
const tempDir = path3.join(
|
|
323
|
+
const tempDir = path3.join(os2.tmpdir(), `nconv-cli-${Date.now()}`);
|
|
140
324
|
try {
|
|
141
325
|
const config = createConfig(options);
|
|
142
326
|
if (config.verbose) {
|
|
@@ -166,10 +350,6 @@ async function mdCommand(notionUrl, options) {
|
|
|
166
350
|
}
|
|
167
351
|
const pageDir = path3.join(config.output, baseFilename);
|
|
168
352
|
await fs3.mkdir(pageDir, { recursive: true });
|
|
169
|
-
if (config.verbose) {
|
|
170
|
-
console.log(`\u{1F4C1} \uCD9C\uB825 \uD3F4\uB354: ${path3.relative(process.cwd(), pageDir)}
|
|
171
|
-
`);
|
|
172
|
-
}
|
|
173
353
|
const imageOutputDir = path3.join(pageDir, config.imageDir);
|
|
174
354
|
await fs3.mkdir(imageOutputDir, { recursive: true });
|
|
175
355
|
if (config.verbose && result.imageFiles.length > 0) {
|
|
@@ -225,10 +405,206 @@ async function mdCommand(notionUrl, options) {
|
|
|
225
405
|
}
|
|
226
406
|
}
|
|
227
407
|
|
|
408
|
+
// src/commands/html.ts
|
|
409
|
+
import path4 from "path";
|
|
410
|
+
import { promises as fs4 } from "fs";
|
|
411
|
+
import os3 from "os";
|
|
412
|
+
async function htmlCommand(notionUrl, options) {
|
|
413
|
+
const tempDir = path4.join(os3.tmpdir(), `nconv-cli-${Date.now()}`);
|
|
414
|
+
try {
|
|
415
|
+
const config = createConfig(options);
|
|
416
|
+
if (config.verbose) {
|
|
417
|
+
info("Configuration loaded successfully");
|
|
418
|
+
console.log(` Output directory: ${config.output}
|
|
419
|
+
`);
|
|
420
|
+
}
|
|
421
|
+
const spinner2 = spinner("Fetching Notion page as HTML...");
|
|
422
|
+
const exporter = new NotionMarkdownExporter({
|
|
423
|
+
tokenV2: config.tokenV2,
|
|
424
|
+
fileToken: config.fileToken
|
|
425
|
+
}, "html");
|
|
426
|
+
let result;
|
|
427
|
+
try {
|
|
428
|
+
result = await exporter.exportHTML(notionUrl, tempDir);
|
|
429
|
+
spinner2.succeed(`Notion page fetched (${result.imageFiles.length} images)`);
|
|
430
|
+
} catch (error2) {
|
|
431
|
+
spinner2.fail("Failed to fetch Notion page");
|
|
432
|
+
throw error2;
|
|
433
|
+
}
|
|
434
|
+
let baseFilename;
|
|
435
|
+
if (config.filename) {
|
|
436
|
+
baseFilename = config.filename.replace(/\.html?$/, "");
|
|
437
|
+
} else {
|
|
438
|
+
const title = extractTitleFromUrl(notionUrl);
|
|
439
|
+
baseFilename = generateSafeFilename(title, "");
|
|
440
|
+
}
|
|
441
|
+
const pageDir = path4.join(config.output, baseFilename);
|
|
442
|
+
await fs4.mkdir(pageDir, { recursive: true });
|
|
443
|
+
const imageOutputDir = path4.join(pageDir, config.imageDir);
|
|
444
|
+
await fs4.mkdir(imageOutputDir, { recursive: true });
|
|
445
|
+
if (config.verbose && result.imageFiles.length > 0) {
|
|
446
|
+
console.log(`Processing image files...
|
|
447
|
+
`);
|
|
448
|
+
}
|
|
449
|
+
let processedHtml = result.html;
|
|
450
|
+
for (const imageFile of result.imageFiles) {
|
|
451
|
+
try {
|
|
452
|
+
const originalFileName = path4.basename(imageFile.filename);
|
|
453
|
+
const safeFileName = originalFileName.replace(/\s+/g, "-");
|
|
454
|
+
const targetPath = path4.join(imageOutputDir, safeFileName);
|
|
455
|
+
await fs4.copyFile(imageFile.sourcePath, targetPath);
|
|
456
|
+
if (config.verbose) {
|
|
457
|
+
console.log(`\u2713 ${safeFileName}`);
|
|
458
|
+
}
|
|
459
|
+
const originalPath = imageFile.filename;
|
|
460
|
+
const relativePath = `./${config.imageDir}/${safeFileName}`;
|
|
461
|
+
const pathParts = originalPath.split("/");
|
|
462
|
+
const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/");
|
|
463
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
464
|
+
processedHtml = processedHtml.replace(new RegExp(`(src|href)="${escapeRegex(originalPath)}"`, "g"), `$1="${relativePath}"`).replace(new RegExp(`(src|href)="${escapeRegex(encodedPath)}"`, "g"), `$1="${relativePath}"`);
|
|
465
|
+
} catch (error2) {
|
|
466
|
+
if (config.verbose) {
|
|
467
|
+
const errorMsg = error2 instanceof Error ? error2.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
|
|
468
|
+
console.error(`\u2717 ${imageFile.filename}: ${errorMsg}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const filename = `${baseFilename}.html`;
|
|
473
|
+
const filePath = path4.join(pageDir, filename);
|
|
474
|
+
await fs4.writeFile(filePath, processedHtml, "utf-8");
|
|
475
|
+
console.log("");
|
|
476
|
+
success("Conversion complete!");
|
|
477
|
+
console.log("");
|
|
478
|
+
console.log(`\u{1F4C1} Folder: ${path4.relative(process.cwd(), pageDir)}`);
|
|
479
|
+
console.log(`\u{1F4C4} HTML: ${filename}`);
|
|
480
|
+
if (result.imageFiles.length > 0) {
|
|
481
|
+
console.log(`\u{1F5BC}\uFE0F Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);
|
|
482
|
+
}
|
|
483
|
+
console.log("");
|
|
484
|
+
} catch (error2) {
|
|
485
|
+
if (error2 instanceof Error) {
|
|
486
|
+
error(error2.message);
|
|
487
|
+
} else {
|
|
488
|
+
error("An unknown error occurred.");
|
|
489
|
+
}
|
|
490
|
+
process.exit(1);
|
|
491
|
+
} finally {
|
|
492
|
+
try {
|
|
493
|
+
await fs4.rm(tempDir, { recursive: true, force: true });
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/commands/pdf.ts
|
|
500
|
+
import path5 from "path";
|
|
501
|
+
import { promises as fs5 } from "fs";
|
|
502
|
+
import os4 from "os";
|
|
503
|
+
async function pdfCommand(notionUrl, options) {
|
|
504
|
+
const tempDir = path5.join(os4.tmpdir(), `nconv-cli-${Date.now()}`);
|
|
505
|
+
try {
|
|
506
|
+
const config = createConfig(options);
|
|
507
|
+
if (config.verbose) {
|
|
508
|
+
info("Configuration loaded successfully");
|
|
509
|
+
console.log(` Output directory: ${config.output}
|
|
510
|
+
`);
|
|
511
|
+
}
|
|
512
|
+
const spinner2 = spinner("Fetching Notion page as Markdown...");
|
|
513
|
+
const exporter = new NotionMarkdownExporter({
|
|
514
|
+
tokenV2: config.tokenV2,
|
|
515
|
+
fileToken: config.fileToken
|
|
516
|
+
});
|
|
517
|
+
let result;
|
|
518
|
+
try {
|
|
519
|
+
result = await exporter.exportWithImages(notionUrl, tempDir);
|
|
520
|
+
spinner2.succeed(`Notion page fetched (${result.imageFiles.length} images)`);
|
|
521
|
+
} catch (error2) {
|
|
522
|
+
spinner2.fail("Failed to fetch Notion page");
|
|
523
|
+
throw error2;
|
|
524
|
+
}
|
|
525
|
+
let baseFilename;
|
|
526
|
+
if (config.filename) {
|
|
527
|
+
baseFilename = config.filename.replace(/\.pdf$/, "");
|
|
528
|
+
} else {
|
|
529
|
+
const title = extractTitleFromUrl(notionUrl);
|
|
530
|
+
baseFilename = generateSafeFilename(title, "");
|
|
531
|
+
}
|
|
532
|
+
const pageDir = path5.join(config.output, baseFilename);
|
|
533
|
+
await fs5.mkdir(pageDir, { recursive: true });
|
|
534
|
+
const imageOutputDir = path5.join(pageDir, config.imageDir);
|
|
535
|
+
await fs5.mkdir(imageOutputDir, { recursive: true });
|
|
536
|
+
if (config.verbose && result.imageFiles.length > 0) {
|
|
537
|
+
console.log(`Processing image files...
|
|
538
|
+
`);
|
|
539
|
+
}
|
|
540
|
+
let processedMarkdown = result.markdown;
|
|
541
|
+
for (const imageFile of result.imageFiles) {
|
|
542
|
+
try {
|
|
543
|
+
const originalFileName = path5.basename(imageFile.filename);
|
|
544
|
+
const safeFileName = originalFileName.replace(/\s+/g, "-");
|
|
545
|
+
const targetPath = path5.join(imageOutputDir, safeFileName);
|
|
546
|
+
await fs5.copyFile(imageFile.sourcePath, targetPath);
|
|
547
|
+
if (config.verbose) {
|
|
548
|
+
console.log(`\u2713 ${safeFileName}`);
|
|
549
|
+
}
|
|
550
|
+
const imageBuffer = await fs5.readFile(targetPath);
|
|
551
|
+
const base64 = imageBuffer.toString("base64");
|
|
552
|
+
const ext = path5.extname(safeFileName).slice(1).toLowerCase();
|
|
553
|
+
const mimeType = ext === "jpg" ? "jpeg" : ext;
|
|
554
|
+
const dataUrl = `data:image/${mimeType};base64,${base64}`;
|
|
555
|
+
const originalPath = imageFile.filename;
|
|
556
|
+
const relativePath = `./${config.imageDir}/${safeFileName}`;
|
|
557
|
+
const pathParts = originalPath.split("/");
|
|
558
|
+
const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/");
|
|
559
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
560
|
+
processedMarkdown = processedMarkdown.replace(new RegExp(`\\(${escapeRegex(originalPath)}\\)`, "g"), `(${dataUrl})`).replace(new RegExp(`\\(${escapeRegex(encodedPath)}\\)`, "g"), `(${dataUrl})`);
|
|
561
|
+
} catch (error2) {
|
|
562
|
+
if (config.verbose) {
|
|
563
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
564
|
+
console.error(`\u2717 ${imageFile.filename}: ${errorMsg}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const pdfSpinner = spinner("Converting Markdown to PDF...");
|
|
569
|
+
const filename = `${baseFilename}.pdf`;
|
|
570
|
+
const pdfPath = path5.join(pageDir, filename);
|
|
571
|
+
try {
|
|
572
|
+
await exporter.exportMarkdownToPDF(processedMarkdown, pdfPath, {
|
|
573
|
+
format: "A4"
|
|
574
|
+
});
|
|
575
|
+
pdfSpinner.succeed("PDF generated successfully");
|
|
576
|
+
} catch (error2) {
|
|
577
|
+
pdfSpinner.fail("Failed to generate PDF");
|
|
578
|
+
throw error2;
|
|
579
|
+
}
|
|
580
|
+
console.log("");
|
|
581
|
+
success("PDF export complete!");
|
|
582
|
+
console.log("");
|
|
583
|
+
console.log(`\u{1F4C1} Folder: ${path5.relative(process.cwd(), pageDir)}`);
|
|
584
|
+
console.log(`\u{1F4C4} PDF: ${filename}`);
|
|
585
|
+
if (result.imageFiles.length > 0) {
|
|
586
|
+
console.log(`\u{1F5BC}\uFE0F Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);
|
|
587
|
+
}
|
|
588
|
+
console.log("");
|
|
589
|
+
} catch (error2) {
|
|
590
|
+
if (error2 instanceof Error) {
|
|
591
|
+
error(error2.message);
|
|
592
|
+
} else {
|
|
593
|
+
error("An unknown error occurred.");
|
|
594
|
+
}
|
|
595
|
+
process.exit(1);
|
|
596
|
+
} finally {
|
|
597
|
+
try {
|
|
598
|
+
await fs5.rm(tempDir, { recursive: true, force: true });
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
228
604
|
// src/commands/debug.ts
|
|
229
|
-
import * as
|
|
230
|
-
import * as
|
|
231
|
-
import * as
|
|
605
|
+
import * as fs6 from "fs/promises";
|
|
606
|
+
import * as os5 from "os";
|
|
607
|
+
import * as path6 from "path";
|
|
232
608
|
async function debugCommand(notionUrl, options) {
|
|
233
609
|
let tempDir;
|
|
234
610
|
try {
|
|
@@ -237,7 +613,7 @@ async function debugCommand(notionUrl, options) {
|
|
|
237
613
|
tokenV2: config.tokenV2,
|
|
238
614
|
fileToken: config.fileToken
|
|
239
615
|
});
|
|
240
|
-
tempDir =
|
|
616
|
+
tempDir = path6.join(os5.tmpdir(), `notion-debug-${Date.now()}`);
|
|
241
617
|
console.log("Fetching Notion page...\n");
|
|
242
618
|
const { markdown, imageFiles } = await exporter.exportWithImages(notionUrl, tempDir);
|
|
243
619
|
console.log("=== Raw Markdown ===\n");
|
|
@@ -253,17 +629,614 @@ async function debugCommand(notionUrl, options) {
|
|
|
253
629
|
console.error("Error:", error2);
|
|
254
630
|
} finally {
|
|
255
631
|
if (tempDir) {
|
|
256
|
-
await
|
|
632
|
+
await fs6.rm(tempDir, { recursive: true, force: true }).catch((err) => {
|
|
257
633
|
console.warn(`Error cleaning up temporary directory: ${err.message}`);
|
|
258
634
|
});
|
|
259
635
|
}
|
|
260
636
|
}
|
|
261
637
|
}
|
|
262
638
|
|
|
639
|
+
// src/commands/init.ts
|
|
640
|
+
import fs7 from "fs";
|
|
641
|
+
import path7 from "path";
|
|
642
|
+
import os6 from "os";
|
|
643
|
+
var handler = async (argv) => {
|
|
644
|
+
const configDir = path7.join(os6.homedir(), ".nconv");
|
|
645
|
+
const configFile = path7.join(configDir, ".env");
|
|
646
|
+
info(`Checking for config file at: ${configFile}`);
|
|
647
|
+
if (fs7.existsSync(configFile)) {
|
|
648
|
+
warn("Configuration file already exists.");
|
|
649
|
+
warn(`If you want to re-initialize, please delete the file first: ${configFile}`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const envContent = `
|
|
653
|
+
# Please provide your Notion access tokens.
|
|
654
|
+
# These are required to fetch content from your Notion pages.
|
|
655
|
+
# You can find these tokens in your browser's cookies when you are logged into Notion.
|
|
656
|
+
TOKEN_V2=
|
|
657
|
+
FILE_TOKEN=
|
|
658
|
+
`.trim();
|
|
659
|
+
try {
|
|
660
|
+
info(`Creating directory at: ${configDir}`);
|
|
661
|
+
if (!fs7.existsSync(configDir)) {
|
|
662
|
+
fs7.mkdirSync(configDir, { recursive: true });
|
|
663
|
+
}
|
|
664
|
+
fs7.writeFileSync(configFile, envContent);
|
|
665
|
+
info("\u2705 Successfully created configuration file.");
|
|
666
|
+
info(`Please edit the file to set your environment variables: ${configFile}`);
|
|
667
|
+
} catch (error2) {
|
|
668
|
+
error("Failed to create configuration file.");
|
|
669
|
+
if (error2 instanceof Error) {
|
|
670
|
+
error(error2.message);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/repl/index.ts
|
|
676
|
+
import prompts from "prompts";
|
|
677
|
+
import chalk2 from "chalk";
|
|
678
|
+
|
|
679
|
+
// src/repl/commands.ts
|
|
680
|
+
import { input as input2, confirm as confirm2 } from "@inquirer/prompts";
|
|
681
|
+
|
|
682
|
+
// src/repl/prompts.ts
|
|
683
|
+
import { input } from "@inquirer/prompts";
|
|
684
|
+
import fs8 from "fs";
|
|
685
|
+
import path8 from "path";
|
|
686
|
+
import os7 from "os";
|
|
687
|
+
function getConfigPath2() {
|
|
688
|
+
return path8.join(os7.homedir(), ".nconv", ".env");
|
|
689
|
+
}
|
|
690
|
+
function getConfigDir() {
|
|
691
|
+
return path8.join(os7.homedir(), ".nconv");
|
|
692
|
+
}
|
|
693
|
+
function loadConfig() {
|
|
694
|
+
const configPath = getConfigPath2();
|
|
695
|
+
if (!fs8.existsSync(configPath)) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const content = fs8.readFileSync(configPath, "utf-8");
|
|
699
|
+
const config = { TOKEN_V2: "", FILE_TOKEN: "" };
|
|
700
|
+
content.split("\n").forEach((line) => {
|
|
701
|
+
const trimmed = line.trim();
|
|
702
|
+
if (trimmed.startsWith("#") || !trimmed) return;
|
|
703
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
704
|
+
const value = valueParts.join("=").trim();
|
|
705
|
+
if (key === "TOKEN_V2") config.TOKEN_V2 = value;
|
|
706
|
+
if (key === "FILE_TOKEN") config.FILE_TOKEN = value;
|
|
707
|
+
});
|
|
708
|
+
return config;
|
|
709
|
+
}
|
|
710
|
+
function saveConfig(config) {
|
|
711
|
+
const configDir = getConfigDir();
|
|
712
|
+
const configPath = getConfigPath2();
|
|
713
|
+
const envContent = `# Notion Access Tokens
|
|
714
|
+
# These tokens are required to fetch content from Notion.
|
|
715
|
+
# You can find them in your browser's cookies when logged into Notion.
|
|
716
|
+
|
|
717
|
+
TOKEN_V2=${config.TOKEN_V2}
|
|
718
|
+
FILE_TOKEN=${config.FILE_TOKEN}
|
|
719
|
+
`;
|
|
720
|
+
try {
|
|
721
|
+
if (!fs8.existsSync(configDir)) {
|
|
722
|
+
fs8.mkdirSync(configDir, { recursive: true });
|
|
723
|
+
}
|
|
724
|
+
fs8.writeFileSync(configPath, envContent);
|
|
725
|
+
info("\u2705 Configuration saved successfully.");
|
|
726
|
+
} catch (error2) {
|
|
727
|
+
error("Failed to save configuration.");
|
|
728
|
+
if (error2 instanceof Error) {
|
|
729
|
+
error(error2.message);
|
|
730
|
+
}
|
|
731
|
+
throw error2;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function promptInitConfig() {
|
|
735
|
+
info("Please enter your Notion access tokens.");
|
|
736
|
+
info("You can find these in your browser cookies when logged into Notion.\n");
|
|
737
|
+
const TOKEN_V2 = await input({
|
|
738
|
+
message: "TOKEN_V2:",
|
|
739
|
+
required: true,
|
|
740
|
+
validate: (value) => {
|
|
741
|
+
if (!value.trim()) return "TOKEN_V2 is required";
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
const FILE_TOKEN = await input({
|
|
746
|
+
message: "FILE_TOKEN:",
|
|
747
|
+
required: true,
|
|
748
|
+
validate: (value) => {
|
|
749
|
+
if (!value.trim()) return "FILE_TOKEN is required";
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
return { TOKEN_V2, FILE_TOKEN };
|
|
754
|
+
}
|
|
755
|
+
async function promptEditConfig(existing) {
|
|
756
|
+
info("Current configuration:\n");
|
|
757
|
+
const TOKEN_V2 = await input({
|
|
758
|
+
message: "TOKEN_V2:",
|
|
759
|
+
default: existing.TOKEN_V2,
|
|
760
|
+
required: true
|
|
761
|
+
});
|
|
762
|
+
const FILE_TOKEN = await input({
|
|
763
|
+
message: "FILE_TOKEN:",
|
|
764
|
+
default: existing.FILE_TOKEN,
|
|
765
|
+
required: true
|
|
766
|
+
});
|
|
767
|
+
return { TOKEN_V2, FILE_TOKEN };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/repl/commands.ts
|
|
771
|
+
async function handleInit() {
|
|
772
|
+
const existing = loadConfig();
|
|
773
|
+
const isFirstTime = !existing || !existing.TOKEN_V2 && !existing.FILE_TOKEN;
|
|
774
|
+
if (!isFirstTime) {
|
|
775
|
+
warn("Configuration already exists.");
|
|
776
|
+
try {
|
|
777
|
+
const overwrite = await confirm2({
|
|
778
|
+
message: "Do you want to overwrite the existing configuration?",
|
|
779
|
+
default: false
|
|
780
|
+
});
|
|
781
|
+
if (!overwrite) {
|
|
782
|
+
info("Configuration unchanged. Use /config to view or edit your settings.");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
} catch (error2) {
|
|
786
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
787
|
+
warn("\nCancelled.");
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
info("\n\u{1F4DD} How to find your Notion tokens\n");
|
|
793
|
+
info("1. Log in to https://notion.so in your browser");
|
|
794
|
+
info("2. Open browser developer tools (press F12)");
|
|
795
|
+
info('3. Go to the "Application" tab');
|
|
796
|
+
info('4. Find "Cookies" section and select https://www.notion.so');
|
|
797
|
+
info('5. Copy the value of "token_v2" cookie \u2192 TOKEN_V2');
|
|
798
|
+
info('6. Copy the value of "file_token" cookie \u2192 FILE_TOKEN\n');
|
|
799
|
+
try {
|
|
800
|
+
const config = await promptInitConfig();
|
|
801
|
+
saveConfig(config);
|
|
802
|
+
} catch (error2) {
|
|
803
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
804
|
+
warn("\nConfiguration cancelled.");
|
|
805
|
+
} else {
|
|
806
|
+
throw error2;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function handleConfig() {
|
|
811
|
+
const existing = loadConfig();
|
|
812
|
+
if (!existing || !existing.TOKEN_V2 && !existing.FILE_TOKEN) {
|
|
813
|
+
warn("No configuration found.");
|
|
814
|
+
info("Run /init to create initial configuration.");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
info("Current configuration:");
|
|
818
|
+
info(`TOKEN_V2: ${existing.TOKEN_V2 ? "***" + existing.TOKEN_V2.slice(-8) : "(not set)"}`);
|
|
819
|
+
info(`FILE_TOKEN: ${existing.FILE_TOKEN ? "***" + existing.FILE_TOKEN.slice(-8) : "(not set)"}
|
|
820
|
+
`);
|
|
821
|
+
try {
|
|
822
|
+
const edit = await input2({
|
|
823
|
+
message: "Edit configuration? (y/n)",
|
|
824
|
+
default: "n"
|
|
825
|
+
});
|
|
826
|
+
if (edit.toLowerCase() === "y" || edit.toLowerCase() === "yes") {
|
|
827
|
+
const newConfig = await promptEditConfig(existing);
|
|
828
|
+
saveConfig(newConfig);
|
|
829
|
+
}
|
|
830
|
+
} catch (error2) {
|
|
831
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
832
|
+
warn("\nEdit cancelled.");
|
|
833
|
+
} else {
|
|
834
|
+
throw error2;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function handleMd(args) {
|
|
839
|
+
let url = "";
|
|
840
|
+
const options = {
|
|
841
|
+
output: "./nconv-output",
|
|
842
|
+
imageDir: "images",
|
|
843
|
+
verbose: false
|
|
844
|
+
};
|
|
845
|
+
if (args.length === 0) {
|
|
846
|
+
try {
|
|
847
|
+
url = await input2({
|
|
848
|
+
message: "Notion URL",
|
|
849
|
+
validate: (value) => {
|
|
850
|
+
if (!value.trim()) return "URL is required";
|
|
851
|
+
if (!value.includes("notion.so") && !value.includes("notion.site")) {
|
|
852
|
+
return "Please enter a valid Notion URL";
|
|
853
|
+
}
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
const outputDir = await input2({
|
|
858
|
+
message: "Output directory [default: ./nconv-output]",
|
|
859
|
+
default: "./nconv-output"
|
|
860
|
+
});
|
|
861
|
+
options.output = outputDir;
|
|
862
|
+
const filename = await input2({
|
|
863
|
+
message: "Filename [leave empty for auto-generated]",
|
|
864
|
+
default: ""
|
|
865
|
+
});
|
|
866
|
+
if (filename.trim()) {
|
|
867
|
+
options.filename = filename;
|
|
868
|
+
}
|
|
869
|
+
options.verbose = await confirm2({
|
|
870
|
+
message: "Enable verbose logging?",
|
|
871
|
+
default: false
|
|
872
|
+
});
|
|
873
|
+
} catch (error2) {
|
|
874
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
875
|
+
warn("\nConversion cancelled.");
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
url = args[0];
|
|
881
|
+
const additionalArgs = args.slice(1);
|
|
882
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
883
|
+
const arg = additionalArgs[i];
|
|
884
|
+
if ((arg === "-o" || arg === "--output") && i + 1 < additionalArgs.length) {
|
|
885
|
+
options.output = additionalArgs[++i];
|
|
886
|
+
} else if ((arg === "-i" || arg === "--image-dir") && i + 1 < additionalArgs.length) {
|
|
887
|
+
options.imageDir = additionalArgs[++i];
|
|
888
|
+
} else if ((arg === "-f" || arg === "--filename") && i + 1 < additionalArgs.length) {
|
|
889
|
+
options.filename = additionalArgs[++i];
|
|
890
|
+
} else if (arg === "-v" || arg === "--verbose") {
|
|
891
|
+
options.verbose = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const configCheck = validateConfig();
|
|
896
|
+
if (!configCheck.valid) {
|
|
897
|
+
error("Cannot convert Notion page:");
|
|
898
|
+
error(configCheck.message || "Configuration is invalid.");
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await mdCommand(url, options);
|
|
902
|
+
}
|
|
903
|
+
async function handleHtml(args) {
|
|
904
|
+
let url = "";
|
|
905
|
+
const options = {
|
|
906
|
+
output: "./nconv-output",
|
|
907
|
+
imageDir: "images",
|
|
908
|
+
verbose: false
|
|
909
|
+
};
|
|
910
|
+
if (args.length === 0) {
|
|
911
|
+
try {
|
|
912
|
+
url = await input2({
|
|
913
|
+
message: "Notion URL",
|
|
914
|
+
validate: (value) => {
|
|
915
|
+
if (!value.trim()) return "URL is required";
|
|
916
|
+
if (!value.includes("notion.so") && !value.includes("notion.site")) {
|
|
917
|
+
return "Please enter a valid Notion URL";
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
const outputDir = await input2({
|
|
923
|
+
message: "Output directory [default: ./nconv-output]",
|
|
924
|
+
default: "./nconv-output"
|
|
925
|
+
});
|
|
926
|
+
options.output = outputDir;
|
|
927
|
+
const filename = await input2({
|
|
928
|
+
message: "Filename [leave empty for auto-generated]",
|
|
929
|
+
default: ""
|
|
930
|
+
});
|
|
931
|
+
if (filename.trim()) {
|
|
932
|
+
options.filename = filename;
|
|
933
|
+
}
|
|
934
|
+
options.verbose = await confirm2({
|
|
935
|
+
message: "Enable verbose logging?",
|
|
936
|
+
default: false
|
|
937
|
+
});
|
|
938
|
+
} catch (error2) {
|
|
939
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
940
|
+
warn("\nConversion cancelled.");
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
url = args[0];
|
|
946
|
+
const additionalArgs = args.slice(1);
|
|
947
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
948
|
+
const arg = additionalArgs[i];
|
|
949
|
+
if ((arg === "-o" || arg === "--output") && i + 1 < additionalArgs.length) {
|
|
950
|
+
options.output = additionalArgs[++i];
|
|
951
|
+
} else if ((arg === "-i" || arg === "--image-dir") && i + 1 < additionalArgs.length) {
|
|
952
|
+
options.imageDir = additionalArgs[++i];
|
|
953
|
+
} else if ((arg === "-f" || arg === "--filename") && i + 1 < additionalArgs.length) {
|
|
954
|
+
options.filename = additionalArgs[++i];
|
|
955
|
+
} else if (arg === "-v" || arg === "--verbose") {
|
|
956
|
+
options.verbose = true;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const configCheck = validateConfig();
|
|
961
|
+
if (!configCheck.valid) {
|
|
962
|
+
error("Cannot convert Notion page:");
|
|
963
|
+
error(configCheck.message || "Configuration is invalid.");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
await htmlCommand(url, options);
|
|
967
|
+
}
|
|
968
|
+
async function handlePdf(args) {
|
|
969
|
+
let url = "";
|
|
970
|
+
const options = {
|
|
971
|
+
output: "./nconv-output",
|
|
972
|
+
imageDir: "images",
|
|
973
|
+
verbose: false
|
|
974
|
+
};
|
|
975
|
+
if (args.length === 0) {
|
|
976
|
+
try {
|
|
977
|
+
url = await input2({
|
|
978
|
+
message: "Notion URL",
|
|
979
|
+
validate: (value) => {
|
|
980
|
+
if (!value.trim()) return "URL is required";
|
|
981
|
+
if (!value.includes("notion.so") && !value.includes("notion.site")) {
|
|
982
|
+
return "Please enter a valid Notion URL";
|
|
983
|
+
}
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
const outputDir = await input2({
|
|
988
|
+
message: "Output directory [default: ./nconv-output]",
|
|
989
|
+
default: "./nconv-output"
|
|
990
|
+
});
|
|
991
|
+
options.output = outputDir;
|
|
992
|
+
const filename = await input2({
|
|
993
|
+
message: "Filename [leave empty for auto-generated]",
|
|
994
|
+
default: ""
|
|
995
|
+
});
|
|
996
|
+
if (filename.trim()) {
|
|
997
|
+
options.filename = filename;
|
|
998
|
+
}
|
|
999
|
+
options.verbose = await confirm2({
|
|
1000
|
+
message: "Enable verbose logging?",
|
|
1001
|
+
default: false
|
|
1002
|
+
});
|
|
1003
|
+
} catch (error2) {
|
|
1004
|
+
if (error2 instanceof Error && error2.message === "User force closed the prompt") {
|
|
1005
|
+
warn("\nConversion cancelled.");
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
url = args[0];
|
|
1011
|
+
const additionalArgs = args.slice(1);
|
|
1012
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
1013
|
+
const arg = additionalArgs[i];
|
|
1014
|
+
if ((arg === "-o" || arg === "--output") && i + 1 < additionalArgs.length) {
|
|
1015
|
+
options.output = additionalArgs[++i];
|
|
1016
|
+
} else if ((arg === "-f" || arg === "--filename") && i + 1 < additionalArgs.length) {
|
|
1017
|
+
options.filename = additionalArgs[++i];
|
|
1018
|
+
} else if (arg === "-v" || arg === "--verbose") {
|
|
1019
|
+
options.verbose = true;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const configCheck = validateConfig();
|
|
1024
|
+
if (!configCheck.valid) {
|
|
1025
|
+
error("Cannot convert Notion page:");
|
|
1026
|
+
error(configCheck.message || "Configuration is invalid.");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
await pdfCommand(url, options);
|
|
1030
|
+
}
|
|
1031
|
+
var AVAILABLE_COMMANDS = [
|
|
1032
|
+
{
|
|
1033
|
+
name: "init",
|
|
1034
|
+
description: "Initialize configuration (set Notion tokens)",
|
|
1035
|
+
examples: ["/init"]
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "config",
|
|
1039
|
+
description: "View and edit current configuration",
|
|
1040
|
+
examples: ["/config"]
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "md",
|
|
1044
|
+
description: "Convert Notion page to markdown",
|
|
1045
|
+
examples: [
|
|
1046
|
+
"/md https://notion.so/page-id",
|
|
1047
|
+
"/md https://notion.so/page-id -o ./blog",
|
|
1048
|
+
"/md https://notion.so/page-id -o ./blog -f my-post -v"
|
|
1049
|
+
]
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "html",
|
|
1053
|
+
description: "Convert Notion page to HTML",
|
|
1054
|
+
examples: [
|
|
1055
|
+
"/html https://notion.so/page-id",
|
|
1056
|
+
"/html https://notion.so/page-id -o ./blog",
|
|
1057
|
+
"/html https://notion.so/page-id -o ./blog -f my-post -v"
|
|
1058
|
+
]
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
name: "pdf",
|
|
1062
|
+
description: "Convert Notion page to PDF (renders actual page)",
|
|
1063
|
+
examples: [
|
|
1064
|
+
"/pdf https://notion.so/page-id",
|
|
1065
|
+
"/pdf https://notion.so/page-id -o ./blog",
|
|
1066
|
+
"/pdf https://notion.so/page-id -o ./blog -f my-post -v"
|
|
1067
|
+
]
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "help",
|
|
1071
|
+
description: "Show this help message",
|
|
1072
|
+
examples: ["/help"]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
name: "exit",
|
|
1076
|
+
description: "Exit the REPL",
|
|
1077
|
+
examples: ["/exit"]
|
|
1078
|
+
}
|
|
1079
|
+
];
|
|
1080
|
+
function getCommandSuggestions(input3) {
|
|
1081
|
+
const cleanInput = input3.toLowerCase().replace(/^\//, "");
|
|
1082
|
+
return AVAILABLE_COMMANDS.filter((cmd) => cmd.name.startsWith(cleanInput)).map((cmd) => `/${cmd.name}`);
|
|
1083
|
+
}
|
|
1084
|
+
function getCommandChoices() {
|
|
1085
|
+
return AVAILABLE_COMMANDS.map((cmd) => ({
|
|
1086
|
+
title: `/${cmd.name}`,
|
|
1087
|
+
value: `/${cmd.name}`,
|
|
1088
|
+
description: cmd.description
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
function findSimilarCommand(input3) {
|
|
1092
|
+
const cleanInput = input3.toLowerCase();
|
|
1093
|
+
for (const cmd of AVAILABLE_COMMANDS) {
|
|
1094
|
+
if (cmd.name.includes(cleanInput) || cleanInput.includes(cmd.name)) {
|
|
1095
|
+
return cmd.name;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
function handleHelp() {
|
|
1101
|
+
info("Available commands:\n");
|
|
1102
|
+
AVAILABLE_COMMANDS.forEach((cmd) => {
|
|
1103
|
+
info(` /${cmd.name.padEnd(20)} ${cmd.description}`);
|
|
1104
|
+
});
|
|
1105
|
+
console.log("");
|
|
1106
|
+
info("Conversion options (for /md, /html, and /pdf):");
|
|
1107
|
+
info(" -o, --output <dir> Output directory (default: ./nconv-output)");
|
|
1108
|
+
info(" -i, --image-dir <dir> Image folder name (default: images) [md/html only]");
|
|
1109
|
+
info(" -f, --filename <name> Output filename");
|
|
1110
|
+
info(" -v, --verbose Enable verbose logging\n");
|
|
1111
|
+
info("Examples:");
|
|
1112
|
+
AVAILABLE_COMMANDS.forEach((cmd) => {
|
|
1113
|
+
cmd.examples.forEach((example) => {
|
|
1114
|
+
info(` ${example}`);
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
console.log("");
|
|
1118
|
+
}
|
|
1119
|
+
async function executeCommand(input3) {
|
|
1120
|
+
const trimmed = input3.trim();
|
|
1121
|
+
if (!trimmed) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
if (!trimmed.startsWith("/")) {
|
|
1125
|
+
error("Commands must start with /");
|
|
1126
|
+
info("Type /help for available commands");
|
|
1127
|
+
info("Example: /init, /md <url>, /config\n");
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
1131
|
+
const command = parts[0].toLowerCase();
|
|
1132
|
+
const args = parts.slice(1);
|
|
1133
|
+
switch (command) {
|
|
1134
|
+
case "init":
|
|
1135
|
+
await handleInit();
|
|
1136
|
+
break;
|
|
1137
|
+
case "config":
|
|
1138
|
+
await handleConfig();
|
|
1139
|
+
break;
|
|
1140
|
+
case "md":
|
|
1141
|
+
await handleMd(args);
|
|
1142
|
+
break;
|
|
1143
|
+
case "html":
|
|
1144
|
+
await handleHtml(args);
|
|
1145
|
+
break;
|
|
1146
|
+
case "pdf":
|
|
1147
|
+
await handlePdf(args);
|
|
1148
|
+
break;
|
|
1149
|
+
case "help":
|
|
1150
|
+
case "h":
|
|
1151
|
+
handleHelp();
|
|
1152
|
+
break;
|
|
1153
|
+
case "exit":
|
|
1154
|
+
case "quit":
|
|
1155
|
+
case "q":
|
|
1156
|
+
return true;
|
|
1157
|
+
default:
|
|
1158
|
+
error(`Unknown command: /${command}`);
|
|
1159
|
+
const similar = findSimilarCommand(command);
|
|
1160
|
+
if (similar) {
|
|
1161
|
+
info(`Did you mean /${similar}?`);
|
|
1162
|
+
}
|
|
1163
|
+
info("Type /help to see all available commands\n");
|
|
1164
|
+
const suggestions = getCommandSuggestions(command);
|
|
1165
|
+
if (suggestions.length > 0 && suggestions.length < 5) {
|
|
1166
|
+
info("Available commands:");
|
|
1167
|
+
suggestions.forEach((cmd) => info(` ${cmd}`));
|
|
1168
|
+
console.log("");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/repl/index.ts
|
|
1175
|
+
function showBanner() {
|
|
1176
|
+
const title = "NCONV CLI (Notion Converter CLI)";
|
|
1177
|
+
const padding = 2;
|
|
1178
|
+
const totalWidth = title.length + padding * 2;
|
|
1179
|
+
const topBorder = "\u2554" + "\u2550".repeat(totalWidth) + "\u2557";
|
|
1180
|
+
const bottomBorder = "\u255A" + "\u2550".repeat(totalWidth) + "\u255D";
|
|
1181
|
+
console.log(chalk2.cyan("\n" + topBorder));
|
|
1182
|
+
console.log(chalk2.cyan("\u2551") + chalk2.bold(" ".repeat(padding) + title + " ".repeat(padding)) + chalk2.cyan("\u2551"));
|
|
1183
|
+
console.log(chalk2.cyan(bottomBorder + "\n"));
|
|
1184
|
+
info("Welcome to nconv interactive mode!");
|
|
1185
|
+
info("Type /help to see available commands");
|
|
1186
|
+
info("Type /exit to quit\n");
|
|
1187
|
+
console.log(chalk2.dim("Quick examples:"));
|
|
1188
|
+
console.log(chalk2.dim(" /init - Set up Notion tokens"));
|
|
1189
|
+
console.log(chalk2.dim(" /md <url> - Convert Notion page"));
|
|
1190
|
+
console.log(chalk2.dim(" /md <url> -o ./blog -f my-post - Convert with options\n"));
|
|
1191
|
+
}
|
|
1192
|
+
async function startRepl() {
|
|
1193
|
+
showBanner();
|
|
1194
|
+
let shouldExit = false;
|
|
1195
|
+
while (!shouldExit) {
|
|
1196
|
+
try {
|
|
1197
|
+
const response = await prompts({
|
|
1198
|
+
type: "autocomplete",
|
|
1199
|
+
name: "command",
|
|
1200
|
+
message: chalk2.cyan("nconv"),
|
|
1201
|
+
choices: getCommandChoices(),
|
|
1202
|
+
suggest: async (input3, choices) => {
|
|
1203
|
+
const searchInput = input3.startsWith("/") ? input3 : `/${input3}`;
|
|
1204
|
+
const filtered = choices.filter(
|
|
1205
|
+
(choice) => choice.title.toLowerCase().startsWith(searchInput.toLowerCase())
|
|
1206
|
+
);
|
|
1207
|
+
if (filtered.length === 0 && input3.trim()) {
|
|
1208
|
+
return Promise.resolve([{ title: input3, value: input3 }]);
|
|
1209
|
+
}
|
|
1210
|
+
return Promise.resolve(filtered);
|
|
1211
|
+
},
|
|
1212
|
+
limit: 5
|
|
1213
|
+
});
|
|
1214
|
+
if (response.command === void 0) {
|
|
1215
|
+
console.log("\n");
|
|
1216
|
+
info("Goodbye! \u{1F44B}");
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
shouldExit = await executeCommand(response.command);
|
|
1220
|
+
if (!shouldExit) {
|
|
1221
|
+
console.log();
|
|
1222
|
+
}
|
|
1223
|
+
} catch (error2) {
|
|
1224
|
+
if (error2 instanceof Error) {
|
|
1225
|
+
error(`Error: ${error2.message}`);
|
|
1226
|
+
console.log();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
info("Exiting nconv...");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
263
1233
|
// src/index.ts
|
|
264
1234
|
var program = new Command();
|
|
265
1235
|
program.name("nconv").description("CLI tool for converting Notion pages to blog-ready markdown").version("1.0.0");
|
|
266
|
-
program.command("
|
|
1236
|
+
program.command("init").description("Create a default .env configuration file").action(async () => {
|
|
1237
|
+
await handler({});
|
|
1238
|
+
});
|
|
1239
|
+
program.command("md <url>").description("Convert a Notion page to markdown").option("-o, --output <dir>", "Output directory", "./nconv-output").option("-i, --image-dir <dir>", "Image folder name (relative to output)", "images").option("-f, --filename <name>", "\uCD9C\uB825 \uD30C\uC77C\uBA85 (\uD655\uC7A5\uC790 \uC81C\uC678 \uB610\uB294 \uD3EC\uD568)").option("-v, --verbose", "Enable verbose logging", false).action(async (url, options) => {
|
|
267
1240
|
await mdCommand(url, {
|
|
268
1241
|
output: options.output,
|
|
269
1242
|
imageDir: options.imageDir,
|
|
@@ -271,8 +1244,25 @@ program.command("md <url>").description("Convert a Notion page to markdown").opt
|
|
|
271
1244
|
verbose: options.verbose
|
|
272
1245
|
});
|
|
273
1246
|
});
|
|
1247
|
+
program.command("html <url>").description("Convert a Notion page to HTML").option("-o, --output <dir>", "Output directory", "./nconv-output").option("-i, --image-dir <dir>", "Image folder name (relative to output)", "images").option("-f, --filename <name>", "Output filename (without extension or with)").option("-v, --verbose", "Enable verbose logging", false).action(async (url, options) => {
|
|
1248
|
+
await htmlCommand(url, {
|
|
1249
|
+
output: options.output,
|
|
1250
|
+
imageDir: options.imageDir,
|
|
1251
|
+
filename: options.filename,
|
|
1252
|
+
verbose: options.verbose
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
program.command("pdf <url>").description("Convert a Notion page to PDF (renders actual Notion page)").option("-o, --output <dir>", "Output directory", "./nconv-output").option("-f, --filename <name>", "Output filename (without extension or with)").option("-v, --verbose", "Enable verbose logging", false).action(async (url, options) => {
|
|
1256
|
+
await pdfCommand(url, {
|
|
1257
|
+
output: options.output,
|
|
1258
|
+
imageDir: "images",
|
|
1259
|
+
// Not used for PDF, but required by interface
|
|
1260
|
+
filename: options.filename,
|
|
1261
|
+
verbose: options.verbose
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
274
1264
|
if (process.env.NODE_ENV !== "production") {
|
|
275
|
-
program.command("debug <url>").description("Debug: Output raw markdown and image URLs").option("-o, --output <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC", "./output").option("-i, --image-dir <dir>", "Image folder name", "images").option("-v, --verbose", "Enable verbose logging", false).action(async (url, options) => {
|
|
1265
|
+
program.command("debug <url>").description("Debug: Output raw markdown and image URLs").option("-o, --output <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC", "./nconv-output").option("-i, --image-dir <dir>", "Image folder name", "images").option("-v, --verbose", "Enable verbose logging", false).action(async (url, options) => {
|
|
276
1266
|
await debugCommand(url, {
|
|
277
1267
|
output: options.output,
|
|
278
1268
|
imageDir: options.imageDir,
|
|
@@ -281,5 +1271,11 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
281
1271
|
});
|
|
282
1272
|
});
|
|
283
1273
|
}
|
|
284
|
-
|
|
1274
|
+
(async () => {
|
|
1275
|
+
if (process.argv.length === 2) {
|
|
1276
|
+
await startRepl();
|
|
1277
|
+
} else {
|
|
1278
|
+
program.parse();
|
|
1279
|
+
}
|
|
1280
|
+
})();
|
|
285
1281
|
//# sourceMappingURL=index.js.map
|