@bytechain.cn/colamd-themes 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.claude/skills/colamd-themes-preview.skill.md +333 -0
  2. package/.claude/skills/colamd-themes.skill.md +10 -0
  3. package/.dsh/skills/colamd-themes/SKILL.md +14 -0
  4. package/.github/supported-topics.yml +17 -0
  5. package/.hermes/skills/tools/colamd-themes/SKILL.md +14 -0
  6. package/.openclaw/skills/colamd-themes/SKILL.md +1 -146
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -145
  9. package/CHANGELOG.md +318 -240
  10. package/DSH_SUPPORT_SUMMARY.md +87 -0
  11. package/README.md +414 -384
  12. package/README_CN.md +412 -384
  13. package/RELEASENOTE.md +541 -365
  14. package/bytechain.cn-colamd-themes-0.5.0.tgz +0 -0
  15. package/dist/cli.js +272 -32
  16. package/dist/cli.js.map +1 -1
  17. package/dist/color-utils.d.ts +1 -0
  18. package/dist/color-utils.js +25 -6
  19. package/dist/color-utils.js.map +1 -1
  20. package/dist/extractors/pdf-extractor.js +6 -1
  21. package/dist/extractors/pdf-extractor.js.map +1 -1
  22. package/dist/extractors/url-extractor.d.ts +0 -4
  23. package/dist/extractors/url-extractor.js +13 -126
  24. package/dist/extractors/url-extractor.js.map +1 -1
  25. package/dist/generator.d.ts +1 -1
  26. package/dist/generator.js +32 -11
  27. package/dist/generator.js.map +1 -1
  28. package/dist/models.d.ts +5 -4
  29. package/dist/models.js +2 -3
  30. package/dist/models.js.map +1 -1
  31. package/dist/renderer.d.ts +4 -2
  32. package/dist/renderer.js +68 -25
  33. package/dist/renderer.js.map +1 -1
  34. package/dist/templates/template.css +220 -498
  35. package/dist/templates/theme-paradigm.md +530 -658
  36. package/dist/templates/theme-template.hbs +256 -370
  37. package/dist/theme-store.js +19 -3
  38. package/dist/theme-store.js.map +1 -1
  39. package/dist/utils/document-paradigm.d.ts +12 -0
  40. package/dist/utils/document-paradigm.js +109 -0
  41. package/dist/utils/document-paradigm.js.map +1 -0
  42. package/dist/validators.d.ts +4 -1
  43. package/dist/validators.js +115 -1
  44. package/dist/validators.js.map +1 -1
  45. package/mcp-server/dist/index.d.ts +13 -0
  46. package/mcp-server/dist/index.d.ts.map +1 -0
  47. package/mcp-server/dist/index.js +0 -0
  48. package/mcp-server/dist/index.js.map +1 -0
  49. package/mcp-server/package-lock.json +1759 -0
  50. package/mcp-server/package.json +24 -0
  51. package/mcp-server/src/index.ts +0 -0
  52. package/mcp-server/tsconfig.json +19 -0
  53. package/package.json +54 -36
  54. package/skills/build-skills.sh +30 -5
  55. package/skills/colamd-themes/SKILL.md +180 -148
  56. package/src/cli.ts +474 -217
  57. package/src/color-systems.ts +289 -289
  58. package/src/color-utils.ts +137 -120
  59. package/src/contrast.ts +66 -66
  60. package/src/docx-style-paradigm.ts +811 -811
  61. package/src/export-cli.ts +356 -356
  62. package/src/extractors/base.ts +8 -8
  63. package/src/extractors/docx-extractor.ts +417 -417
  64. package/src/extractors/pdf-extractor.ts +533 -527
  65. package/src/extractors/url-extractor.ts +846 -981
  66. package/src/generator.ts +197 -169
  67. package/src/index.ts +29 -29
  68. package/src/models.ts +212 -212
  69. package/src/renderer.ts +332 -288
  70. package/src/templates/template.css +220 -498
  71. package/src/templates/theme-paradigm.md +530 -658
  72. package/src/templates/theme-template.hbs +256 -370
  73. package/src/theme-store.ts +185 -170
  74. package/src/types/pdf-parse.d.ts +12 -12
  75. package/src/utils/cli-utils.ts +84 -84
  76. package/src/utils/document-paradigm.ts +118 -0
  77. package/src/validators.ts +298 -176
  78. package/themes/academic-paper-pt.css +353 -1805
  79. package/themes/academic-paper.css +350 -1099
  80. package/themes/academic-style.css +342 -2430
  81. package/themes/colly.css +256 -370
  82. package/themes/elegant.css +288 -671
  83. package/themes/forest-ink.css +240 -664
  84. package/themes/guizang.css +250 -732
  85. package/themes/hermes-security.css +279 -0
  86. package/themes/meeting-minutes.css +256 -370
  87. package/themes/pixso-design.css +278 -1261
  88. package/themes/salvator-blog.css +256 -370
  89. package/themes/shimo-theme.css +256 -0
  90. package/themes/swiss-design.css +285 -746
  91. package/themes/template.css +220 -498
  92. package/themes/trae-blue.css +280 -737
  93. package/themes/word-contract.css +249 -0
  94. package/themes-preview/demo-content.md +187 -187
  95. package/tsconfig.json +18 -18
  96. package/src/mermaid-presets.ts +0 -142
  97. package/themes/swiss-design copy.css +0 -746
  98. package/themes-preview/colly-preview.html +0 -623
  99. package/themes-preview/meeting-minutes-preview.html +0 -625
  100. package/themes-preview/salvator-blog-preview.html +0 -623
package/src/export-cli.ts CHANGED
@@ -1,356 +1,356 @@
1
- /**
2
- * Export CLI commands: set-theme, export-html, export-pdf, export (batch).
3
- *
4
- * These are registered onto the main Commander program in cli.ts.
5
- * Uses unified error handling via CLIError and shared utility functions.
6
- */
7
-
8
- import { Command } from "commander";
9
- import { readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
10
- import { join, resolve } from "node:path";
11
- import chalk from "chalk";
12
- import ora from "ora";
13
- import {
14
- BUILTIN_THEMES,
15
- getConfig,
16
- setDefaultTheme as storeSetDefault,
17
- registerTheme as storeRegister,
18
- unregisterTheme as storeUnregister,
19
- resolveTheme,
20
- } from "./theme-store.js";
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";
29
-
30
- // ── set-theme ──
31
-
32
- function registerSetTheme(program: Command): void {
33
- program
34
- .command("set-theme")
35
- .description("Set or register a theme for export")
36
- .argument("<name>", "Theme name (built-in or custom)")
37
- .option("--css <file>", "Register as custom theme using this CSS file")
38
- .option("--default", "Set as the default export theme")
39
- .option("--remove", "Unregister a custom theme")
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;
51
- }
52
-
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;
63
- }
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
- }
76
- }
77
-
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);
95
- }
96
- }
97
- });
98
- }
99
-
100
- // ── export-html ──
101
-
102
- function registerExportHTML(program: Command): void {
103
- program
104
- .command("export-html")
105
- .description("Export a Markdown file to standalone HTML with theme applied")
106
- .argument("<input>", "Path to .md file")
107
- .option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
108
- .option("-o, --output <path>", "Output HTML file path")
109
- .action(async (input: string, options: { theme?: string; output?: string }) => {
110
- try {
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
- }
144
- } catch (err) {
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
- }
151
- }
152
- });
153
- }
154
-
155
- // ── export-pdf ──
156
-
157
- function registerExportPDF(program: Command): void {
158
- program
159
- .command("export-pdf")
160
- .description("Export a Markdown file to PDF with theme applied")
161
- .argument("<input>", "Path to .md file")
162
- .option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
163
- .option("-o, --output <path>", "Output PDF file path")
164
- .option("--format <size>", "Page format: A4, Letter, A3", "A4")
165
- .action(async (input: string, options: { theme?: string; output?: string; format?: string }) => {
166
- try {
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
- }
183
-
184
- // Resolve theme with async API
185
- const theme = await resolveTheme(options.theme);
186
-
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
- }
212
- } catch (err) {
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
- }
219
- }
220
- });
221
- }
222
-
223
- // ── export (batch) ──
224
-
225
- function registerExport(program: Command): void {
226
- program
227
- .command("export")
228
- .description("Batch export Markdown files to HTML or PDF")
229
- .argument("<inputs...>", "Paths to .md files (supports globs)")
230
- .option("--format <type>", "Output format: html or pdf", "html")
231
- .option("-t, --theme <name>", "Theme name")
232
- .option("-d, --output-dir <dir>", "Output directory", ".")
233
- .action(async (inputs: string[], options: { format?: string; theme?: string; outputDir?: string }) => {
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}`));
251
- }
252
- }
253
-
254
- if (files.length === 0) {
255
- throw new CLIError("No valid input files found.", ErrorCode.FILE_NOT_FOUND);
256
- }
257
-
258
- // Resolve theme with async API
259
- const theme = await resolveTheme(options.theme);
260
-
261
- // Setup output directory
262
- const outDir = resolve(process.cwd(), options.outputDir ?? ".");
263
- const fmt = options.format ?? "html";
264
-
265
- if (!["html", "pdf"].includes(fmt)) {
266
- throw new CLIError(
267
- `Invalid format: "${fmt}". Supported formats: html, pdf`,
268
- ErrorCode.INVALID_INPUT
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++;
317
- }
318
- }
319
- } finally {
320
- await renderer.close();
321
- }
322
-
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
- }
345
- });
346
- }
347
-
348
- /**
349
- * Register all export-related commands onto the program.
350
- */
351
- export function registerExportCommands(program: Command): void {
352
- registerSetTheme(program);
353
- registerExportHTML(program);
354
- registerExportPDF(program);
355
- registerExport(program);
356
- }
1
+ /**
2
+ * Export CLI commands: set-theme, export-html, export-pdf, export (batch).
3
+ *
4
+ * These are registered onto the main Commander program in cli.ts.
5
+ * Uses unified error handling via CLIError and shared utility functions.
6
+ */
7
+
8
+ import { Command } from "commander";
9
+ import { readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
10
+ import { join, resolve } from "node:path";
11
+ import chalk from "chalk";
12
+ import ora from "ora";
13
+ import {
14
+ BUILTIN_THEMES,
15
+ getConfig,
16
+ setDefaultTheme as storeSetDefault,
17
+ registerTheme as storeRegister,
18
+ unregisterTheme as storeUnregister,
19
+ resolveTheme,
20
+ } from "./theme-store.js";
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";
29
+
30
+ // ── set-theme ──
31
+
32
+ function registerSetTheme(program: Command): void {
33
+ program
34
+ .command("set-theme")
35
+ .description("Set or register a theme for export")
36
+ .argument("<name>", "Theme name (built-in or custom)")
37
+ .option("--css <file>", "Register as custom theme using this CSS file")
38
+ .option("--default", "Set as the default export theme")
39
+ .option("--remove", "Unregister a custom theme")
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;
51
+ }
52
+
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;
63
+ }
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
+ }
76
+ }
77
+
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);
95
+ }
96
+ }
97
+ });
98
+ }
99
+
100
+ // ── export-html ──
101
+
102
+ function registerExportHTML(program: Command): void {
103
+ program
104
+ .command("export-html")
105
+ .description("Export a Markdown file to standalone HTML with theme applied")
106
+ .argument("<input>", "Path to .md file")
107
+ .option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
108
+ .option("-o, --output <path>", "Output HTML file path")
109
+ .action(async (input: string, options: { theme?: string; output?: string }) => {
110
+ try {
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
+ }
144
+ } catch (err) {
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
+ }
151
+ }
152
+ });
153
+ }
154
+
155
+ // ── export-pdf ──
156
+
157
+ function registerExportPDF(program: Command): void {
158
+ program
159
+ .command("export-pdf")
160
+ .description("Export a Markdown file to PDF with theme applied")
161
+ .argument("<input>", "Path to .md file")
162
+ .option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
163
+ .option("-o, --output <path>", "Output PDF file path")
164
+ .option("--format <size>", "Page format: A4, Letter, A3", "A4")
165
+ .action(async (input: string, options: { theme?: string; output?: string; format?: string }) => {
166
+ try {
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
+ }
183
+
184
+ // Resolve theme with async API
185
+ const theme = await resolveTheme(options.theme);
186
+
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
+ }
212
+ } catch (err) {
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
+ }
219
+ }
220
+ });
221
+ }
222
+
223
+ // ── export (batch) ──
224
+
225
+ function registerExport(program: Command): void {
226
+ program
227
+ .command("export")
228
+ .description("Batch export Markdown files to HTML or PDF")
229
+ .argument("<inputs...>", "Paths to .md files (supports globs)")
230
+ .option("--format <type>", "Output format: html or pdf", "html")
231
+ .option("-t, --theme <name>", "Theme name")
232
+ .option("-d, --output-dir <dir>", "Output directory", ".")
233
+ .action(async (inputs: string[], options: { format?: string; theme?: string; outputDir?: string }) => {
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}`));
251
+ }
252
+ }
253
+
254
+ if (files.length === 0) {
255
+ throw new CLIError("No valid input files found.", ErrorCode.FILE_NOT_FOUND);
256
+ }
257
+
258
+ // Resolve theme with async API
259
+ const theme = await resolveTheme(options.theme);
260
+
261
+ // Setup output directory
262
+ const outDir = resolve(process.cwd(), options.outputDir ?? ".");
263
+ const fmt = options.format ?? "html";
264
+
265
+ if (!["html", "pdf"].includes(fmt)) {
266
+ throw new CLIError(
267
+ `Invalid format: "${fmt}". Supported formats: html, pdf`,
268
+ ErrorCode.INVALID_INPUT
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++;
317
+ }
318
+ }
319
+ } finally {
320
+ await renderer.close();
321
+ }
322
+
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
+ }
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Register all export-related commands onto the program.
350
+ */
351
+ export function registerExportCommands(program: Command): void {
352
+ registerSetTheme(program);
353
+ registerExportHTML(program);
354
+ registerExportPDF(program);
355
+ registerExport(program);
356
+ }