@bytechain.cn/colamd-themes 0.3.0 → 0.3.2

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 (39) hide show
  1. package/.openclaw/skills/colamd-themes/SKILL.md +15 -124
  2. package/.trae/rules/colamd-themes.md +15 -124
  3. package/CHANGELOG.md +142 -3
  4. package/README.md +12 -1
  5. package/README_CN.md +12 -1
  6. package/RELEASENOTE.md +212 -0
  7. package/dist/cli.js +278 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/export-cli.d.ts +1 -0
  10. package/dist/export-cli.js +208 -176
  11. package/dist/export-cli.js.map +1 -1
  12. package/dist/extractors/url-extractor.d.ts +6 -0
  13. package/dist/extractors/url-extractor.js +51 -2
  14. package/dist/extractors/url-extractor.js.map +1 -1
  15. package/dist/generator.d.ts +4 -0
  16. package/dist/generator.js +74 -7
  17. package/dist/generator.js.map +1 -1
  18. package/dist/renderer.d.ts +1 -1
  19. package/dist/renderer.js +87 -14
  20. package/dist/renderer.js.map +1 -1
  21. package/dist/theme-store.d.ts +7 -6
  22. package/dist/theme-store.js +84 -34
  23. package/dist/theme-store.js.map +1 -1
  24. package/dist/utils/cli-utils.d.ts +55 -0
  25. package/dist/utils/cli-utils.js +80 -0
  26. package/dist/utils/cli-utils.js.map +1 -0
  27. package/package.json +1 -1
  28. package/skills/colamd-themes/SKILL.md +30 -0
  29. package/src/cli.ts +289 -2
  30. package/src/export-cli.ts +248 -193
  31. package/src/extractors/url-extractor.ts +56 -2
  32. package/src/generator.ts +88 -7
  33. package/src/renderer.ts +96 -14
  34. package/src/theme-store.ts +93 -35
  35. package/src/utils/cli-utils.ts +84 -0
  36. package/.claude/skills/colamd-themes-preview.skill.md +0 -333
  37. package/.claude/skills/colamd-themes.skill.md +0 -155
  38. package/.hermes/skills/tools/colamd-themes/SKILL.md +0 -159
  39. package/.opencode/skills/colamd-themes/SKILL.md +0 -162
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 { existsSync, readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
9
- import { join, isAbsolute, resolve, basename } from "node:path";
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
- // Remove
34
- if (options.remove) {
35
- if (storeUnregister(name)) {
36
- console.log(chalk.green(`✔ Unregistered custom theme "${name}"`));
37
- } else {
38
- console.log(chalk.yellow(`Theme "${name}" is not registered.`));
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
- // Register custom theme
44
- if (options.css) {
45
- const cssPath = isAbsolute(options.css) ? options.css : resolve(process.cwd(), options.css);
46
- if (!existsSync(cssPath)) {
47
- console.error(chalk.red(`CSS file not found: ${cssPath}`));
48
- process.exit(1);
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
- storeRegister(name, cssPath);
51
- console.log(chalk.green(`✔ Registered custom theme "${name}" → ${cssPath}`));
52
- if (options.default) {
53
- storeSetDefault(name);
54
- console.log(chalk.green(`✔ Set "${name}" as default theme`));
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
- // Set as default (built-in or already registered)
60
- if (!BUILTIN_THEMES.includes(name as any)) {
61
- const config = getConfig();
62
- if (!(name in config.themes)) {
63
- console.error(
64
- chalk.red(`Unknown theme "${name}". `) +
65
- chalk.dim(`Built-in: ${BUILTIN_THEMES.join(", ")}. `) +
66
- chalk.dim("Or register a custom theme with --css <file>.")
67
- );
68
- process.exit(1);
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
- const { renderMarkdown } = await import("./renderer.js");
117
- const result = await renderMarkdown({
118
- markdown: md,
119
- themeName: theme.themeName,
120
- customCSS: theme.customCSS,
121
- title,
122
- });
123
-
124
- const { writeFileSync } = await import("node:fs");
125
- writeFileSync(outputPath, result.html, "utf-8");
126
- spinner.succeed(chalk.green(`Exported: ${outputPath}`));
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
- spinner.fail(chalk.red(`Export failed: ${(err as Error).message}`));
129
- process.exit(1);
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
- theme = resolveTheme(options.theme);
160
- } catch (err) {
161
- console.error(chalk.red((err as Error).message));
162
- process.exit(1);
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
- const spinner = ora(`Rendering ${basename(inputPath)} → PDF (${theme.themeName})...`).start();
166
- try {
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
- const { writeFileSync } = await import("node:fs");
179
- writeFileSync(outputPath, pdfBuffer);
180
- spinner.succeed(chalk.green(`Exported: ${outputPath}`));
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
- spinner.fail(chalk.red(`Export failed: ${(err as Error).message}`));
183
- process.exit(1);
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
- // Resolve glob inputs (shell usually expands, but handle edge cases)
200
- const files: string[] = [];
201
- for (const input of inputs) {
202
- const p = isAbsolute(input) ? input : resolve(process.cwd(), input);
203
- if (existsSync(p)) {
204
- if (statSync(p).isDirectory()) {
205
- // Directory: include all .md files
206
- const dirFiles = readdirSync(p)
207
- .filter((f) => f.endsWith(".md"))
208
- .map((f) => join(p, f));
209
- files.push(...dirFiles);
210
- } else {
211
- files.push(p);
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
- const outDir = isAbsolute(options.outputDir ?? ".")
232
- ? (options.outputDir ?? ".")
233
- : resolve(process.cwd(), options.outputDir ?? ".");
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
- try {
248
- await renderer.init();
258
+ // Resolve theme with async API
259
+ const theme = await resolveTheme(options.theme);
249
260
 
250
- for (const file of files) {
251
- const name = basename(file, ".md");
252
- const md = readFileSync(file, "utf-8");
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
- try {
257
- const input: RenderInput = {
258
- markdown: md,
259
- themeName: theme.themeName,
260
- customCSS: theme.customCSS,
261
- title: name,
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
- if (fmt === "pdf") {
265
- const pdfBuffer = await renderer.exportPDF(input);
266
- const { writeFileSync } = await import("node:fs");
267
- writeFileSync(outPath, pdfBuffer);
268
- } else {
269
- const result = await renderer.render(input);
270
- const { writeFileSync } = await import("node:fs");
271
- writeFileSync(outPath, result.html, "utf-8");
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
- console.log(
286
- chalk.bold(`\nDone: ${chalk.green(`${success} exported`)}`) +
287
- (failed ? chalk.red(`, ${failed} failed`) : "") +
288
- "\n"
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
 
@@ -56,11 +56,65 @@ export class UrlExtractor implements Extractor {
56
56
  return /^https?:\/\//i.test(source);
57
57
  }
58
58
 
59
+ /**
60
+ * Validate URL to prevent SSRF attacks and ensure security.
61
+ * - Only allows http/https protocols
62
+ * - Blocks private network addresses (localhost, 10.x, 172.16-31.x, 192.168.x, 169.254.x)
63
+ */
64
+ private validateUrl(url: string): void {
65
+ let parsed: URL;
66
+ try {
67
+ parsed = new URL(url);
68
+ } catch {
69
+ throw new Error(`Invalid URL format: ${url}`);
70
+ }
71
+
72
+ // Protocol whitelist
73
+ if (!["http:", "https:"].includes(parsed.protocol)) {
74
+ throw new Error(`Unsupported protocol "${parsed.protocol}". Only HTTP and HTTPS are allowed.`);
75
+ }
76
+
77
+ // Block private network ranges to prevent SSRF
78
+ const hostname = parsed.hostname.toLowerCase();
79
+ const isPrivate =
80
+ hostname === "localhost" ||
81
+ hostname === "127.0.0.1" ||
82
+ hostname === "::1" ||
83
+ hostname === "[::1]" ||
84
+ hostname.startsWith("10.") ||
85
+ hostname.startsWith("172.16.") ||
86
+ hostname.startsWith("172.17.") ||
87
+ hostname.startsWith("172.18.") ||
88
+ hostname.startsWith("172.19.") ||
89
+ hostname.startsWith("172.20.") ||
90
+ hostname.startsWith("172.21.") ||
91
+ hostname.startsWith("172.22.") ||
92
+ hostname.startsWith("172.23.") ||
93
+ hostname.startsWith("172.24.") ||
94
+ hostname.startsWith("172.25.") ||
95
+ hostname.startsWith("172.26.") ||
96
+ hostname.startsWith("172.27.") ||
97
+ hostname.startsWith("172.28.") ||
98
+ hostname.startsWith("172.29.") ||
99
+ hostname.startsWith("172.30.") ||
100
+ hostname.startsWith("172.31.") ||
101
+ hostname.startsWith("192.168.") ||
102
+ hostname.startsWith("169.254.");
103
+
104
+ if (isPrivate) {
105
+ throw new Error(`Private network access denied: ${hostname}. Only public URLs are allowed for security reasons.`);
106
+ }
107
+ }
108
+
59
109
  async extract(source: string): Promise<ThemeStyle> {
110
+ this.validateUrl(source);
111
+
60
112
  const response = await axios.get(source, {
61
- headers: { "User-Agent": "ColaMD-Themes/1.0 (Theme Extractor)" },
113
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; ColaMD-Themes/1.0)" },
62
114
  timeout: 15000,
63
- maxRedirects: 5,
115
+ maxRedirects: 3,
116
+ maxContentLength: 10 * 1024 * 1024,
117
+ responseType: "text",
64
118
  });
65
119
 
66
120
  const contentType = String(response.headers["content-type"] || "");