@bytechain.cn/colamd-themes 0.3.0 → 0.3.1
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/CHANGELOG.md +139 -1
- package/README.md +12 -1
- package/README_CN.md +12 -1
- package/RELEASENOTE.md +188 -0
- package/dist/cli.js +19 -2
- package/dist/cli.js.map +1 -1
- package/dist/export-cli.d.ts +1 -0
- package/dist/export-cli.js +208 -176
- package/dist/export-cli.js.map +1 -1
- package/dist/extractors/url-extractor.d.ts +6 -0
- package/dist/extractors/url-extractor.js +51 -2
- package/dist/extractors/url-extractor.js.map +1 -1
- package/dist/generator.d.ts +4 -0
- package/dist/generator.js +74 -7
- package/dist/generator.js.map +1 -1
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +21 -11
- package/dist/renderer.js.map +1 -1
- package/dist/theme-store.d.ts +7 -6
- package/dist/theme-store.js +84 -34
- package/dist/theme-store.js.map +1 -1
- package/dist/utils/cli-utils.d.ts +55 -0
- package/dist/utils/cli-utils.js +80 -0
- package/dist/utils/cli-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +19 -2
- package/src/export-cli.ts +248 -193
- package/src/extractors/url-extractor.ts +56 -2
- package/src/generator.ts +88 -7
- package/src/renderer.ts +22 -11
- package/src/theme-store.ts +93 -35
- package/src/utils/cli-utils.ts +84 -0
- package/.claude/skills/colamd-themes-preview.skill.md +0 -333
- package/.claude/skills/colamd-themes.skill.md +0 -155
- package/.hermes/skills/tools/colamd-themes/SKILL.md +0 -159
- package/.opencode/skills/colamd-themes/SKILL.md +0 -162
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions for CLI commands.
|
|
3
|
+
* Provides consistent file path resolution and error handling.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { resolve, isAbsolute, basename } from "node:path";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve input file path with validation.
|
|
9
|
+
* Converts relative paths to absolute and checks existence.
|
|
10
|
+
*
|
|
11
|
+
* @param input File path provided by user
|
|
12
|
+
* @returns Resolved absolute path
|
|
13
|
+
* @throws Error if file does not exist
|
|
14
|
+
*/
|
|
15
|
+
export function resolveInputPath(input) {
|
|
16
|
+
const path = isAbsolute(input) ? input : resolve(process.cwd(), input);
|
|
17
|
+
if (!existsSync(path)) {
|
|
18
|
+
throw new Error(`File not found: ${path}`);
|
|
19
|
+
}
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve output file path with sensible defaults.
|
|
24
|
+
* If output path is not specified, derives it from input path by replacing extension.
|
|
25
|
+
*
|
|
26
|
+
* @param inputPath Resolved absolute input path
|
|
27
|
+
* @param output Optional explicit output path
|
|
28
|
+
* @param ext Target extension (default: ".html")
|
|
29
|
+
* @returns Resolved absolute output path
|
|
30
|
+
*/
|
|
31
|
+
export function resolveOutputPath(inputPath, output, ext = ".html") {
|
|
32
|
+
if (output) {
|
|
33
|
+
return isAbsolute(output) ? output : resolve(process.cwd(), output);
|
|
34
|
+
}
|
|
35
|
+
return inputPath.replace(/\.md$/i, ext);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get base filename without extension for use as document title.
|
|
39
|
+
*
|
|
40
|
+
* @param filePath Absolute or relative file path
|
|
41
|
+
* @returns Filename without extension
|
|
42
|
+
*/
|
|
43
|
+
export function getDocumentTitle(filePath) {
|
|
44
|
+
return basename(filePath, ".md");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Standard error codes for CLI operations.
|
|
48
|
+
* Enables consistent error handling across all commands.
|
|
49
|
+
*/
|
|
50
|
+
export var ErrorCode;
|
|
51
|
+
(function (ErrorCode) {
|
|
52
|
+
/** Input file not found */
|
|
53
|
+
ErrorCode[ErrorCode["FILE_NOT_FOUND"] = 1] = "FILE_NOT_FOUND";
|
|
54
|
+
/** Theme resolution failed */
|
|
55
|
+
ErrorCode[ErrorCode["THEME_ERROR"] = 2] = "THEME_ERROR";
|
|
56
|
+
/** Rendering/export failed */
|
|
57
|
+
ErrorCode[ErrorCode["RENDER_ERROR"] = 3] = "RENDER_ERROR";
|
|
58
|
+
/** Invalid arguments or configuration */
|
|
59
|
+
ErrorCode[ErrorCode["INVALID_INPUT"] = 4] = "INVALID_INPUT";
|
|
60
|
+
/** Unknown/unexpected error */
|
|
61
|
+
ErrorCode[ErrorCode["UNKNOWN"] = 99] = "UNKNOWN";
|
|
62
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
63
|
+
/**
|
|
64
|
+
* Custom error class for CLI operations with structured error codes.
|
|
65
|
+
* Replaces direct process.exit() calls for better error propagation.
|
|
66
|
+
*/
|
|
67
|
+
export class CLIError extends Error {
|
|
68
|
+
code;
|
|
69
|
+
cause;
|
|
70
|
+
constructor(message, code = ErrorCode.UNKNOWN, cause) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.code = code;
|
|
73
|
+
this.cause = cause;
|
|
74
|
+
this.name = "CLIError";
|
|
75
|
+
if (cause) {
|
|
76
|
+
this.stack = `${this.message}\nCaused by: ${cause.stack}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=cli-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-utils.js","sourceRoot":"","sources":["../../src/utils/cli-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;IACvE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,MAAe,EAAE,GAAG,GAAG,OAAO;IACjF,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAN,IAAY,SAWX;AAXD,WAAY,SAAS;IACnB,2BAA2B;IAC3B,6DAAkB,CAAA;IAClB,8BAA8B;IAC9B,uDAAe,CAAA;IACf,8BAA8B;IAC9B,yDAAgB,CAAA;IAChB,yCAAyC;IACzC,2DAAiB,CAAA;IACjB,+BAA+B;IAC/B,gDAAY,CAAA;AACd,CAAC,EAXW,SAAS,KAAT,SAAS,QAWpB;AAED;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGxB;IACA;IAHT,YACE,OAAe,EACR,OAAkB,SAAS,CAAC,OAAO,EACnC,KAAa;QAEpB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,SAAI,GAAJ,IAAI,CAA+B;QACnC,UAAK,GAAL,KAAK,CAAQ;QAGpB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,OAAO,gBAAgB,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5D,CAAC;IACH,CAAC;CACF"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, isAbsolute } from "node:path";
|
|
5
|
+
import { join, isAbsolute, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import chalk from "chalk";
|
|
7
8
|
import ora from "ora";
|
|
8
9
|
import { UrlExtractor } from "./extractors/url-extractor.js";
|
|
@@ -16,12 +17,28 @@ import { validateSeedPalette, summarizeIssues } from "./validators.js";
|
|
|
16
17
|
import type { ThemeStyle } from "./models.js";
|
|
17
18
|
import { registerExportCommands } from "./export-cli.js";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Dynamically read version from package.json to avoid hardcoding.
|
|
22
|
+
* This ensures CLI --version always matches the published package version.
|
|
23
|
+
*/
|
|
24
|
+
function getPackageVersion(): string {
|
|
25
|
+
try {
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = dirname(__filename);
|
|
28
|
+
const pkgPath = join(__dirname, "../package.json");
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
30
|
+
return pkg.version ?? "0.0.0";
|
|
31
|
+
} catch {
|
|
32
|
+
return "0.0.0";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
const program = new Command();
|
|
20
37
|
|
|
21
38
|
program
|
|
22
39
|
.name("colamd-themes")
|
|
23
40
|
.description("ColaMD Theme Development Tool — extract formatting and generate v3.0 paradigm CSS themes")
|
|
24
|
-
.version(
|
|
41
|
+
.version(getPackageVersion());
|
|
25
42
|
|
|
26
43
|
// ── Shared extract options builder ──
|
|
27
44
|
|
package/src/export-cli.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Export CLI commands: set-theme, export-html, export-pdf, export (batch).
|
|
3
3
|
*
|
|
4
4
|
* These are registered onto the main Commander program in cli.ts.
|
|
5
|
+
* Uses unified error handling via CLIError and shared utility functions.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Command } from "commander";
|
|
8
|
-
import {
|
|
9
|
-
import { join,
|
|
9
|
+
import { readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
10
11
|
import chalk from "chalk";
|
|
11
12
|
import ora from "ora";
|
|
12
13
|
import {
|
|
@@ -18,6 +19,13 @@ import {
|
|
|
18
19
|
resolveTheme,
|
|
19
20
|
} from "./theme-store.js";
|
|
20
21
|
import { ColamdRenderer, type RenderInput } from "./renderer.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveInputPath,
|
|
24
|
+
resolveOutputPath,
|
|
25
|
+
getDocumentTitle,
|
|
26
|
+
CLIError,
|
|
27
|
+
ErrorCode,
|
|
28
|
+
} from "./utils/cli-utils.js";
|
|
21
29
|
|
|
22
30
|
// ── set-theme ──
|
|
23
31
|
|
|
@@ -29,54 +37,62 @@ function registerSetTheme(program: Command): void {
|
|
|
29
37
|
.option("--css <file>", "Register as custom theme using this CSS file")
|
|
30
38
|
.option("--default", "Set as the default export theme")
|
|
31
39
|
.option("--remove", "Unregister a custom theme")
|
|
32
|
-
.action((name: string, options: { css?: string; default?: boolean; remove?: boolean }) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
.action(async (name: string, options: { css?: string; default?: boolean; remove?: boolean }) => {
|
|
41
|
+
try {
|
|
42
|
+
// Remove
|
|
43
|
+
if (options.remove) {
|
|
44
|
+
const removed = await storeUnregister(name);
|
|
45
|
+
if (removed) {
|
|
46
|
+
console.log(chalk.green(`✔ Unregistered custom theme "${name}"`));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.yellow(`Theme "${name}" is not registered.`));
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
39
51
|
}
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.
|
|
48
|
-
|
|
53
|
+
// Register custom theme
|
|
54
|
+
if (options.css) {
|
|
55
|
+
const cssPath = resolveInputPath(options.css);
|
|
56
|
+
await storeRegister(name, cssPath);
|
|
57
|
+
console.log(chalk.green(`✔ Registered custom theme "${name}" → ${cssPath}`));
|
|
58
|
+
if (options.default) {
|
|
59
|
+
await storeSetDefault(name);
|
|
60
|
+
console.log(chalk.green(`✔ Set "${name}" as default theme`));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
|
|
65
|
+
// Set as default (built-in or already registered)
|
|
66
|
+
if (!BUILTIN_THEMES.includes(name as any)) {
|
|
67
|
+
const config = await getConfig();
|
|
68
|
+
if (!(name in config.themes)) {
|
|
69
|
+
throw new CLIError(
|
|
70
|
+
`Unknown theme "${name}". ` +
|
|
71
|
+
chalk.dim(`Built-in: ${BUILTIN_THEMES.join(", ")}. `) +
|
|
72
|
+
chalk.dim("Or register a custom theme with --css <file>."),
|
|
73
|
+
ErrorCode.THEME_ERROR
|
|
74
|
+
);
|
|
75
|
+
}
|
|
55
76
|
}
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
);
|
|
68
|
-
|
|
78
|
+
await storeSetDefault(name);
|
|
79
|
+
console.log(chalk.green(`✔ Default theme set to "${name}"`));
|
|
80
|
+
|
|
81
|
+
// Show current config
|
|
82
|
+
const config = await getConfig();
|
|
83
|
+
console.log(chalk.dim(`\n Default: ${config.defaultTheme}`));
|
|
84
|
+
const customs = Object.keys(config.themes);
|
|
85
|
+
if (customs.length) {
|
|
86
|
+
console.log(chalk.dim(` Custom themes: ${customs.join(", ")}`));
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof CLIError) {
|
|
90
|
+
console.error(chalk.red(err.message));
|
|
91
|
+
process.exit(err.code);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(chalk.red(`Unexpected error: ${(err as Error).message}`));
|
|
94
|
+
process.exit(ErrorCode.UNKNOWN);
|
|
69
95
|
}
|
|
70
|
-
}
|
|
71
|
-
storeSetDefault(name);
|
|
72
|
-
console.log(chalk.green(`✔ Default theme set to "${name}"`));
|
|
73
|
-
|
|
74
|
-
// Show current config
|
|
75
|
-
const config = getConfig();
|
|
76
|
-
console.log(chalk.dim(`\n Default: ${config.defaultTheme}`));
|
|
77
|
-
const customs = Object.keys(config.themes);
|
|
78
|
-
if (customs.length) {
|
|
79
|
-
console.log(chalk.dim(` Custom themes: ${customs.join(", ")}`));
|
|
80
96
|
}
|
|
81
97
|
});
|
|
82
98
|
}
|
|
@@ -91,42 +107,47 @@ function registerExportHTML(program: Command): void {
|
|
|
91
107
|
.option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
|
|
92
108
|
.option("-o, --output <path>", "Output HTML file path")
|
|
93
109
|
.action(async (input: string, options: { theme?: string; output?: string }) => {
|
|
94
|
-
const inputPath = isAbsolute(input) ? input : resolve(process.cwd(), input);
|
|
95
|
-
if (!existsSync(inputPath)) {
|
|
96
|
-
console.error(chalk.red(`File not found: ${inputPath}`));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const md = readFileSync(inputPath, "utf-8");
|
|
101
|
-
const title = basename(inputPath, ".md");
|
|
102
|
-
const outputPath = options.output
|
|
103
|
-
? (isAbsolute(options.output) ? options.output : resolve(process.cwd(), options.output))
|
|
104
|
-
: inputPath.replace(/\.md$/i, ".html");
|
|
105
|
-
|
|
106
|
-
let theme;
|
|
107
|
-
try {
|
|
108
|
-
theme = resolveTheme(options.theme);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
console.error(chalk.red((err as Error).message));
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const spinner = ora(`Rendering ${basename(inputPath)} → HTML (${theme.themeName})...`).start();
|
|
115
110
|
try {
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
// Resolve paths using shared utilities
|
|
112
|
+
const inputPath = resolveInputPath(input);
|
|
113
|
+
const title = getDocumentTitle(inputPath);
|
|
114
|
+
const outputPath = resolveOutputPath(inputPath, options.output, ".html");
|
|
115
|
+
|
|
116
|
+
// Read markdown content
|
|
117
|
+
const md = readFileSync(inputPath, "utf-8");
|
|
118
|
+
|
|
119
|
+
// Resolve theme with async API
|
|
120
|
+
const theme = await resolveTheme(options.theme);
|
|
121
|
+
|
|
122
|
+
// Render with progress indicator
|
|
123
|
+
const spinner = ora(`Rendering ${title} → HTML (${theme.themeName})...`).start();
|
|
124
|
+
try {
|
|
125
|
+
const { renderMarkdown } = await import("./renderer.js");
|
|
126
|
+
const result = await renderMarkdown({
|
|
127
|
+
markdown: md,
|
|
128
|
+
themeName: theme.themeName,
|
|
129
|
+
customCSS: theme.customCSS,
|
|
130
|
+
title,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const { writeFileSync } = await import("node:fs");
|
|
134
|
+
writeFileSync(outputPath, result.html, "utf-8");
|
|
135
|
+
spinner.succeed(chalk.green(`Exported: ${outputPath}`));
|
|
136
|
+
} catch (renderErr) {
|
|
137
|
+
spinner.fail(chalk.red(`Export failed: ${(renderErr as Error).message}`));
|
|
138
|
+
throw new CLIError(
|
|
139
|
+
`HTML rendering failed: ${(renderErr as Error).message}`,
|
|
140
|
+
ErrorCode.RENDER_ERROR,
|
|
141
|
+
renderErr as Error
|
|
142
|
+
);
|
|
143
|
+
}
|
|
127
144
|
} catch (err) {
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
if (err instanceof CLIError) {
|
|
146
|
+
process.exit(err.code);
|
|
147
|
+
} else {
|
|
148
|
+
console.error(chalk.red((err as Error).message));
|
|
149
|
+
process.exit(ErrorCode.FILE_NOT_FOUND);
|
|
150
|
+
}
|
|
130
151
|
}
|
|
131
152
|
});
|
|
132
153
|
}
|
|
@@ -142,45 +163,59 @@ function registerExportPDF(program: Command): void {
|
|
|
142
163
|
.option("-o, --output <path>", "Output PDF file path")
|
|
143
164
|
.option("--format <size>", "Page format: A4, Letter, A3", "A4")
|
|
144
165
|
.action(async (input: string, options: { theme?: string; output?: string; format?: string }) => {
|
|
145
|
-
const inputPath = isAbsolute(input) ? input : resolve(process.cwd(), input);
|
|
146
|
-
if (!existsSync(inputPath)) {
|
|
147
|
-
console.error(chalk.red(`File not found: ${inputPath}`));
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const md = readFileSync(inputPath, "utf-8");
|
|
152
|
-
const title = basename(inputPath, ".md");
|
|
153
|
-
const outputPath = options.output
|
|
154
|
-
? (isAbsolute(options.output) ? options.output : resolve(process.cwd(), options.output))
|
|
155
|
-
: inputPath.replace(/\.md$/i, ".pdf");
|
|
156
|
-
|
|
157
|
-
let theme;
|
|
158
166
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
// Resolve paths using shared utilities
|
|
168
|
+
const inputPath = resolveInputPath(input);
|
|
169
|
+
const title = getDocumentTitle(inputPath);
|
|
170
|
+
const outputPath = resolveOutputPath(inputPath, options.output, ".pdf");
|
|
171
|
+
|
|
172
|
+
// Read markdown content
|
|
173
|
+
const md = readFileSync(inputPath, "utf-8");
|
|
174
|
+
|
|
175
|
+
// Validate format option
|
|
176
|
+
const validFormats = ["A4", "Letter", "A3"];
|
|
177
|
+
if (!validFormats.includes(options.format ?? "")) {
|
|
178
|
+
throw new CLIError(
|
|
179
|
+
`Invalid page format: "${options.format}". Valid formats: ${validFormats.join(", ")}`,
|
|
180
|
+
ErrorCode.INVALID_INPUT
|
|
181
|
+
);
|
|
182
|
+
}
|
|
164
183
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const { renderPDF } = await import("./renderer.js");
|
|
168
|
-
const pdfBuffer = await renderPDF(
|
|
169
|
-
{
|
|
170
|
-
markdown: md,
|
|
171
|
-
themeName: theme.themeName,
|
|
172
|
-
customCSS: theme.customCSS,
|
|
173
|
-
title,
|
|
174
|
-
},
|
|
175
|
-
{ format: options.format as any }
|
|
176
|
-
);
|
|
184
|
+
// Resolve theme with async API
|
|
185
|
+
const theme = await resolveTheme(options.theme);
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
// Render PDF with progress indicator
|
|
188
|
+
const spinner = ora(`Rendering ${title} → PDF (${theme.themeName})...`).start();
|
|
189
|
+
try {
|
|
190
|
+
const { renderPDF } = await import("./renderer.js");
|
|
191
|
+
const pdfBuffer = await renderPDF(
|
|
192
|
+
{
|
|
193
|
+
markdown: md,
|
|
194
|
+
themeName: theme.themeName,
|
|
195
|
+
customCSS: theme.customCSS,
|
|
196
|
+
title,
|
|
197
|
+
},
|
|
198
|
+
{ format: options.format as any }
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const { writeFileSync } = await import("node:fs");
|
|
202
|
+
writeFileSync(outputPath, pdfBuffer);
|
|
203
|
+
spinner.succeed(chalk.green(`Exported: ${outputPath}`));
|
|
204
|
+
} catch (renderErr) {
|
|
205
|
+
spinner.fail(chalk.red(`Export failed: ${(renderErr as Error).message}`));
|
|
206
|
+
throw new CLIError(
|
|
207
|
+
`PDF rendering failed: ${(renderErr as Error).message}`,
|
|
208
|
+
ErrorCode.RENDER_ERROR,
|
|
209
|
+
renderErr as Error
|
|
210
|
+
);
|
|
211
|
+
}
|
|
181
212
|
} catch (err) {
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
if (err instanceof CLIError) {
|
|
214
|
+
process.exit(err.code);
|
|
215
|
+
} else {
|
|
216
|
+
console.error(chalk.red((err as Error).message));
|
|
217
|
+
process.exit(ErrorCode.FILE_NOT_FOUND);
|
|
218
|
+
}
|
|
184
219
|
}
|
|
185
220
|
});
|
|
186
221
|
}
|
|
@@ -196,97 +231,117 @@ function registerExport(program: Command): void {
|
|
|
196
231
|
.option("-t, --theme <name>", "Theme name")
|
|
197
232
|
.option("-d, --output-dir <dir>", "Output directory", ".")
|
|
198
233
|
.action(async (inputs: string[], options: { format?: string; theme?: string; outputDir?: string }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
try {
|
|
235
|
+
// Resolve glob inputs (shell usually expands, but handle edge cases)
|
|
236
|
+
const files: string[] = [];
|
|
237
|
+
for (const input of inputs) {
|
|
238
|
+
try {
|
|
239
|
+
const p = resolveInputPath(input);
|
|
240
|
+
if (statSync(p).isDirectory()) {
|
|
241
|
+
// Directory: include all .md files
|
|
242
|
+
const dirFiles = readdirSync(p)
|
|
243
|
+
.filter((f) => f.endsWith(".md"))
|
|
244
|
+
.map((f) => join(p, f));
|
|
245
|
+
files.push(...dirFiles);
|
|
246
|
+
} else {
|
|
247
|
+
files.push(p);
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
console.log(chalk.yellow(`Skipping (not found): ${input}`));
|
|
212
251
|
}
|
|
213
|
-
} else {
|
|
214
|
-
console.log(chalk.yellow(`Skipping (not found): ${input}`));
|
|
215
252
|
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (files.length === 0) {
|
|
219
|
-
console.error(chalk.red("No valid input files found."));
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
let theme;
|
|
224
|
-
try {
|
|
225
|
-
theme = resolveTheme(options.theme);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
console.error(chalk.red((err as Error).message));
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
253
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const fmt = options.format ?? "html";
|
|
235
|
-
|
|
236
|
-
// Ensure output directory exists
|
|
237
|
-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
238
|
-
|
|
239
|
-
console.log(chalk.bold(`\nBatch export: ${files.length} file(s) → ${fmt.toUpperCase()}`));
|
|
240
|
-
console.log(chalk.dim(` Theme: ${theme.themeName}`));
|
|
241
|
-
console.log(chalk.dim(` Output: ${outDir}/\n`));
|
|
242
|
-
|
|
243
|
-
const renderer = new ColamdRenderer();
|
|
244
|
-
let success = 0;
|
|
245
|
-
let failed = 0;
|
|
254
|
+
if (files.length === 0) {
|
|
255
|
+
throw new CLIError("No valid input files found.", ErrorCode.FILE_NOT_FOUND);
|
|
256
|
+
}
|
|
246
257
|
|
|
247
|
-
|
|
248
|
-
await
|
|
258
|
+
// Resolve theme with async API
|
|
259
|
+
const theme = await resolveTheme(options.theme);
|
|
249
260
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const outPath = join(outDir, `${name}.${fmt === "pdf" ? "pdf" : "html"}`);
|
|
254
|
-
const spinner = ora(` ${name}.md`).start();
|
|
261
|
+
// Setup output directory
|
|
262
|
+
const outDir = resolve(process.cwd(), options.outputDir ?? ".");
|
|
263
|
+
const fmt = options.format ?? "html";
|
|
255
264
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
};
|
|
265
|
+
if (!["html", "pdf"].includes(fmt)) {
|
|
266
|
+
throw new CLIError(
|
|
267
|
+
`Invalid format: "${fmt}". Supported formats: html, pdf`,
|
|
268
|
+
ErrorCode.INVALID_INPUT
|
|
269
|
+
);
|
|
270
|
+
}
|
|
263
271
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
+
// Ensure output directory exists
|
|
273
|
+
const { existsSync: dirExists } = await import("node:fs");
|
|
274
|
+
if (!dirExists(outDir)) mkdirSync(outDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
console.log(chalk.bold(`\nBatch export: ${files.length} file(s) → ${fmt.toUpperCase()}`));
|
|
277
|
+
console.log(chalk.dim(` Theme: ${theme.themeName}`));
|
|
278
|
+
console.log(chalk.dim(` Output: ${outDir}/\n`));
|
|
279
|
+
|
|
280
|
+
// Initialize renderer for batch processing
|
|
281
|
+
const renderer = new ColamdRenderer();
|
|
282
|
+
let success = 0;
|
|
283
|
+
let failed = 0;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await renderer.init();
|
|
287
|
+
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const name = getDocumentTitle(file);
|
|
290
|
+
const md = readFileSync(file, "utf-8");
|
|
291
|
+
const outPath = join(outDir, `${name}.${fmt === "pdf" ? "pdf" : "html"}`);
|
|
292
|
+
const spinner = ora(` ${name}.md`).start();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const input: RenderInput = {
|
|
296
|
+
markdown: md,
|
|
297
|
+
themeName: theme.themeName,
|
|
298
|
+
customCSS: theme.customCSS,
|
|
299
|
+
title: name,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (fmt === "pdf") {
|
|
303
|
+
const pdfBuffer = await renderer.exportPDF(input);
|
|
304
|
+
const { writeFileSync } = await import("node:fs");
|
|
305
|
+
writeFileSync(outPath, pdfBuffer);
|
|
306
|
+
} else {
|
|
307
|
+
const result = await renderer.render(input);
|
|
308
|
+
const { writeFileSync } = await import("node:fs");
|
|
309
|
+
writeFileSync(outPath, result.html, "utf-8");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
spinner.succeed(`${name}.${fmt === "pdf" ? "pdf" : "html"}`);
|
|
313
|
+
success++;
|
|
314
|
+
} catch (fileErr) {
|
|
315
|
+
spinner.fail(`${name}.md — ${(fileErr as Error).message}`);
|
|
316
|
+
failed++;
|
|
272
317
|
}
|
|
273
|
-
|
|
274
|
-
spinner.succeed(`${name}.${fmt === "pdf" ? "pdf" : "html"}`);
|
|
275
|
-
success++;
|
|
276
|
-
} catch (err) {
|
|
277
|
-
spinner.fail(`${name}.md — ${(err as Error).message}`);
|
|
278
|
-
failed++;
|
|
279
318
|
}
|
|
319
|
+
} finally {
|
|
320
|
+
await renderer.close();
|
|
280
321
|
}
|
|
281
|
-
} finally {
|
|
282
|
-
await renderer.close();
|
|
283
|
-
}
|
|
284
322
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
323
|
+
// Summary
|
|
324
|
+
console.log(
|
|
325
|
+
chalk.bold(`\nDone: ${chalk.green(`${success} exported`)}`) +
|
|
326
|
+
(failed ? chalk.red(`, ${failed} failed`) : "") +
|
|
327
|
+
"\n"
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (failed > 0) {
|
|
331
|
+
throw new CLIError(
|
|
332
|
+
`${failed} file(s) failed to export`,
|
|
333
|
+
ErrorCode.RENDER_ERROR
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err instanceof CLIError) {
|
|
338
|
+
console.error(chalk.red(err.message));
|
|
339
|
+
process.exit(err.code);
|
|
340
|
+
} else {
|
|
341
|
+
console.error(chalk.red((err as Error).message));
|
|
342
|
+
process.exit(ErrorCode.UNKNOWN);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
290
345
|
});
|
|
291
346
|
}
|
|
292
347
|
|