@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.
- package/.claude/skills/colamd-themes-preview.skill.md +333 -0
- package/.claude/skills/colamd-themes.skill.md +10 -0
- package/.dsh/skills/colamd-themes/SKILL.md +14 -0
- package/.github/supported-topics.yml +17 -0
- package/.hermes/skills/tools/colamd-themes/SKILL.md +14 -0
- package/.openclaw/skills/colamd-themes/SKILL.md +1 -146
- package/.opencode/skills/colamd-themes/SKILL.md +17 -0
- package/.trae/rules/colamd-themes.md +0 -145
- package/CHANGELOG.md +318 -240
- package/DSH_SUPPORT_SUMMARY.md +87 -0
- package/README.md +414 -384
- package/README_CN.md +412 -384
- package/RELEASENOTE.md +541 -365
- package/bytechain.cn-colamd-themes-0.5.0.tgz +0 -0
- package/dist/cli.js +272 -32
- package/dist/cli.js.map +1 -1
- package/dist/color-utils.d.ts +1 -0
- package/dist/color-utils.js +25 -6
- package/dist/color-utils.js.map +1 -1
- package/dist/extractors/pdf-extractor.js +6 -1
- package/dist/extractors/pdf-extractor.js.map +1 -1
- package/dist/extractors/url-extractor.d.ts +0 -4
- package/dist/extractors/url-extractor.js +13 -126
- package/dist/extractors/url-extractor.js.map +1 -1
- package/dist/generator.d.ts +1 -1
- package/dist/generator.js +32 -11
- package/dist/generator.js.map +1 -1
- package/dist/models.d.ts +5 -4
- package/dist/models.js +2 -3
- package/dist/models.js.map +1 -1
- package/dist/renderer.d.ts +4 -2
- package/dist/renderer.js +68 -25
- package/dist/renderer.js.map +1 -1
- package/dist/templates/template.css +220 -498
- package/dist/templates/theme-paradigm.md +530 -658
- package/dist/templates/theme-template.hbs +256 -370
- package/dist/theme-store.js +19 -3
- package/dist/theme-store.js.map +1 -1
- package/dist/utils/document-paradigm.d.ts +12 -0
- package/dist/utils/document-paradigm.js +109 -0
- package/dist/utils/document-paradigm.js.map +1 -0
- package/dist/validators.d.ts +4 -1
- package/dist/validators.js +115 -1
- package/dist/validators.js.map +1 -1
- package/mcp-server/dist/index.d.ts +13 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +0 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/package-lock.json +1759 -0
- package/mcp-server/package.json +24 -0
- package/mcp-server/src/index.ts +0 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +54 -36
- package/skills/build-skills.sh +30 -5
- package/skills/colamd-themes/SKILL.md +180 -148
- package/src/cli.ts +474 -217
- package/src/color-systems.ts +289 -289
- package/src/color-utils.ts +137 -120
- package/src/contrast.ts +66 -66
- package/src/docx-style-paradigm.ts +811 -811
- package/src/export-cli.ts +356 -356
- package/src/extractors/base.ts +8 -8
- package/src/extractors/docx-extractor.ts +417 -417
- package/src/extractors/pdf-extractor.ts +533 -527
- package/src/extractors/url-extractor.ts +846 -981
- package/src/generator.ts +197 -169
- package/src/index.ts +29 -29
- package/src/models.ts +212 -212
- package/src/renderer.ts +332 -288
- package/src/templates/template.css +220 -498
- package/src/templates/theme-paradigm.md +530 -658
- package/src/templates/theme-template.hbs +256 -370
- package/src/theme-store.ts +185 -170
- package/src/types/pdf-parse.d.ts +12 -12
- package/src/utils/cli-utils.ts +84 -84
- package/src/utils/document-paradigm.ts +118 -0
- package/src/validators.ts +298 -176
- package/themes/academic-paper-pt.css +353 -1805
- package/themes/academic-paper.css +350 -1099
- package/themes/academic-style.css +342 -2430
- package/themes/colly.css +256 -370
- package/themes/elegant.css +288 -671
- package/themes/forest-ink.css +240 -664
- package/themes/guizang.css +250 -732
- package/themes/hermes-security.css +279 -0
- package/themes/meeting-minutes.css +256 -370
- package/themes/pixso-design.css +278 -1261
- package/themes/salvator-blog.css +256 -370
- package/themes/shimo-theme.css +256 -0
- package/themes/swiss-design.css +285 -746
- package/themes/template.css +220 -498
- package/themes/trae-blue.css +280 -737
- package/themes/word-contract.css +249 -0
- package/themes-preview/demo-content.md +187 -187
- package/tsconfig.json +18 -18
- package/src/mermaid-presets.ts +0 -142
- package/themes/swiss-design copy.css +0 -746
- package/themes-preview/colly-preview.html +0 -623
- package/themes-preview/meeting-minutes-preview.html +0 -625
- 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
|
+
}
|