@bytechain.cn/colamd-themes 0.3.2 → 0.5.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.
Files changed (99) 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 -37
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -36
  9. package/CHANGELOG.md +331 -241
  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 -389
  14. package/dist/cli.js +36 -55
  15. package/dist/cli.js.map +1 -1
  16. package/dist/color-utils.d.ts +1 -0
  17. package/dist/color-utils.js +25 -6
  18. package/dist/color-utils.js.map +1 -1
  19. package/dist/extractors/pdf-extractor.js +6 -1
  20. package/dist/extractors/pdf-extractor.js.map +1 -1
  21. package/dist/extractors/url-extractor.d.ts +0 -4
  22. package/dist/extractors/url-extractor.js +13 -126
  23. package/dist/extractors/url-extractor.js.map +1 -1
  24. package/dist/generator.d.ts +1 -1
  25. package/dist/generator.js +32 -11
  26. package/dist/generator.js.map +1 -1
  27. package/dist/models.d.ts +5 -4
  28. package/dist/models.js +2 -3
  29. package/dist/models.js.map +1 -1
  30. package/dist/renderer.d.ts +6 -3
  31. package/dist/renderer.js +54 -93
  32. package/dist/renderer.js.map +1 -1
  33. package/dist/templates/template.css +220 -498
  34. package/dist/templates/theme-paradigm.md +530 -658
  35. package/dist/templates/theme-template.hbs +256 -370
  36. package/dist/theme-store.js +19 -3
  37. package/dist/theme-store.js.map +1 -1
  38. package/dist/utils/document-paradigm.d.ts +12 -0
  39. package/dist/utils/document-paradigm.js +109 -0
  40. package/dist/utils/document-paradigm.js.map +1 -0
  41. package/dist/validators.d.ts +4 -1
  42. package/dist/validators.js +115 -1
  43. package/dist/validators.js.map +1 -1
  44. package/mcp-server/dist/index.d.ts +13 -0
  45. package/mcp-server/dist/index.d.ts.map +1 -0
  46. package/mcp-server/dist/index.js +0 -0
  47. package/mcp-server/dist/index.js.map +1 -0
  48. package/mcp-server/package-lock.json +1759 -0
  49. package/mcp-server/package.json +24 -0
  50. package/mcp-server/src/index.ts +0 -0
  51. package/mcp-server/tsconfig.json +19 -0
  52. package/package.json +54 -36
  53. package/skills/build-skills.sh +30 -5
  54. package/skills/colamd-themes/SKILL.md +180 -178
  55. package/src/cli.ts +474 -487
  56. package/src/color-systems.ts +289 -289
  57. package/src/color-utils.ts +137 -120
  58. package/src/contrast.ts +66 -66
  59. package/src/docx-style-paradigm.ts +811 -811
  60. package/src/export-cli.ts +356 -356
  61. package/src/extractors/base.ts +8 -8
  62. package/src/extractors/docx-extractor.ts +417 -417
  63. package/src/extractors/pdf-extractor.ts +533 -527
  64. package/src/extractors/url-extractor.ts +846 -981
  65. package/src/generator.ts +197 -169
  66. package/src/index.ts +29 -29
  67. package/src/models.ts +212 -212
  68. package/src/renderer.ts +310 -359
  69. package/src/templates/template.css +220 -498
  70. package/src/templates/theme-paradigm.md +530 -658
  71. package/src/templates/theme-template.hbs +256 -370
  72. package/src/theme-store.ts +185 -170
  73. package/src/types/pdf-parse.d.ts +12 -12
  74. package/src/utils/cli-utils.ts +84 -84
  75. package/src/utils/document-paradigm.ts +118 -0
  76. package/src/validators.ts +298 -176
  77. package/themes/academic-paper-pt.css +353 -1805
  78. package/themes/academic-paper.css +350 -1099
  79. package/themes/academic-style.css +342 -2430
  80. package/themes/colly.css +256 -370
  81. package/themes/elegant.css +288 -671
  82. package/themes/forest-ink.css +240 -664
  83. package/themes/guizang.css +250 -732
  84. package/themes/hermes-security.css +279 -0
  85. package/themes/meeting-minutes.css +256 -370
  86. package/themes/pixso-design.css +278 -1261
  87. package/themes/salvator-blog.css +256 -370
  88. package/themes/shimo-theme.css +256 -0
  89. package/themes/swiss-design.css +285 -746
  90. package/themes/template.css +220 -498
  91. package/themes/trae-blue.css +280 -737
  92. package/themes/word-contract.css +249 -0
  93. package/themes-preview/demo-content.md +187 -187
  94. package/tsconfig.json +18 -18
  95. package/src/mermaid-presets.ts +0 -142
  96. package/themes/swiss-design copy.css +0 -746
  97. package/themes-preview/colly-preview.html +0 -623
  98. package/themes-preview/meeting-minutes-preview.html +0 -625
  99. 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
+ }