@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.
- 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 -37
- package/.opencode/skills/colamd-themes/SKILL.md +17 -0
- package/.trae/rules/colamd-themes.md +0 -36
- package/CHANGELOG.md +331 -241
- package/DSH_SUPPORT_SUMMARY.md +87 -0
- package/README.md +414 -384
- package/README_CN.md +412 -384
- package/RELEASENOTE.md +541 -389
- package/dist/cli.js +36 -55
- 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 +6 -3
- package/dist/renderer.js +54 -93
- 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 -178
- package/src/cli.ts +474 -487
- 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 +310 -359
- 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/cli.ts
CHANGED
|
@@ -1,487 +1,474 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, isAbsolute, dirname, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { execSync } from "node:child_process";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import ora from "ora";
|
|
10
|
-
import { UrlExtractor } from "./extractors/url-extractor.js";
|
|
11
|
-
import { DocxExtractor } from "./extractors/docx-extractor.js";
|
|
12
|
-
import { PdfExtractor } from "./extractors/pdf-extractor.js";
|
|
13
|
-
import type { Extractor } from "./extractors/base.js";
|
|
14
|
-
import { writeThemeFile } from "./generator.js";
|
|
15
|
-
import { COLOR_SYSTEMS, getColorSystem } from "./color-systems.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const css = readFileSync(cssPath, "utf-8");
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (options.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
function printSummary(style: ThemeStyle): void {
|
|
477
|
-
const s = style.seed;
|
|
478
|
-
console.log(chalk.dim("\n Seed palette:"));
|
|
479
|
-
console.log(` ${chalk.white("surface:")} ${s.surface} ${chalk.white("ink:")} ${s.ink} ${chalk.white("accent:")} ${s.accent}`);
|
|
480
|
-
console.log(` ${chalk.white("panel:")} ${s.panel} ${chalk.white("panel-alt:")} ${s.panelAlt}`);
|
|
481
|
-
if (style.colorSystemId) {
|
|
482
|
-
console.log(chalk.dim(` Color system: ${style.colorSystemId} Mermaid: ${style.mermaidPresetId}`));
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export function main(): void { program.parse(); }
|
|
487
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join, isAbsolute, dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import { UrlExtractor } from "./extractors/url-extractor.js";
|
|
11
|
+
import { DocxExtractor } from "./extractors/docx-extractor.js";
|
|
12
|
+
import { PdfExtractor } from "./extractors/pdf-extractor.js";
|
|
13
|
+
import type { Extractor } from "./extractors/base.js";
|
|
14
|
+
import { writeThemeFile } from "./generator.js";
|
|
15
|
+
import { COLOR_SYSTEMS, getColorSystem } from "./color-systems.js";
|
|
16
|
+
import { validateSeedPalette, validateThemeCss, summarizeIssues } from "./validators.js";
|
|
17
|
+
import type { ThemeStyle } from "./models.js";
|
|
18
|
+
import { registerExportCommands } from "./export-cli.js";
|
|
19
|
+
import { BUILTIN_THEMES } from "./theme-store.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dynamically read version from package.json to avoid hardcoding.
|
|
23
|
+
* This ensures CLI --version always matches the published package version.
|
|
24
|
+
*/
|
|
25
|
+
function getPackageVersion(): string {
|
|
26
|
+
try {
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const pkgPath = join(__dirname, "../package.json");
|
|
30
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
31
|
+
return pkg.version ?? "0.0.0";
|
|
32
|
+
} catch {
|
|
33
|
+
return "0.0.0";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Fallback stacks used when colamd.css is missing or a theme rule can't be parsed. */
|
|
38
|
+
const BUILTIN_FONT_FALLBACKS: Record<string, string[]> = {
|
|
39
|
+
light: ["Noto Sans SC", "sans-serif"],
|
|
40
|
+
dark: ["Noto Sans SC", "sans-serif"],
|
|
41
|
+
elegant: ["LXGW WenKai", "Noto Serif SC", "serif"],
|
|
42
|
+
newsprint: ["Noto Serif SC", "serif"],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Font stacks for each built-in theme — parsed dynamically from colamd.css. */
|
|
46
|
+
const THEME_FONTS: Record<string, string[]> = getBuiltinFontStacks();
|
|
47
|
+
|
|
48
|
+
/** Find the path to @bytechain.cn/colamd/dist/lib/colamd.css */
|
|
49
|
+
function findColamdCssPath(): string {
|
|
50
|
+
const candidates = [
|
|
51
|
+
resolve(dirname(new URL(import.meta.url).pathname), "../node_modules/@bytechain.cn/colamd/dist/lib/colamd.css"),
|
|
52
|
+
resolve(process.cwd(), "node_modules/@bytechain.cn/colamd/dist/lib/colamd.css"),
|
|
53
|
+
];
|
|
54
|
+
for (const p of candidates) {
|
|
55
|
+
if (existsSync(p)) return p;
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse font-family values for each built-in theme from colamd.css.
|
|
62
|
+
* Dynamic approach: reads `body.theme-{name} ... .cm-content { font-family: ... }`
|
|
63
|
+
* instead of hardcoding font stacks.
|
|
64
|
+
*/
|
|
65
|
+
function getBuiltinFontStacks(): Record<string, string[]> {
|
|
66
|
+
const result: Record<string, string[]> = { ...BUILTIN_FONT_FALLBACKS };
|
|
67
|
+
const cssPath = findColamdCssPath();
|
|
68
|
+
if (!cssPath) return result;
|
|
69
|
+
|
|
70
|
+
const css = readFileSync(cssPath, "utf-8");
|
|
71
|
+
|
|
72
|
+
for (const theme of Object.keys(BUILTIN_FONT_FALLBACKS)) {
|
|
73
|
+
// 2.7 契约:字体栈写在 .cm-content 上(CodeMirror 6 不再有 .ProseMirror)
|
|
74
|
+
const re = new RegExp(
|
|
75
|
+
`body\\.theme-${theme}[^{]*\\.cm-content[^{]*\\{[^}]*font-family:\\s*([^;]+)`,
|
|
76
|
+
"i"
|
|
77
|
+
);
|
|
78
|
+
const m = css.match(re);
|
|
79
|
+
if (!m || m[1].includes("var(")) continue;
|
|
80
|
+
|
|
81
|
+
const fonts = m[1]
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, "").trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
if (fonts.length > 0) result[theme] = fonts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if a single font name is available on the system.
|
|
93
|
+
* Multi-platform: fc-match (Linux/macOS/WSL), PowerShell registry (Windows),
|
|
94
|
+
* system_profiler (macOS), universal directory scan fallback.
|
|
95
|
+
*/
|
|
96
|
+
function checkFont(fontName: string): string | null {
|
|
97
|
+
// ── try fc-match first (Linux, WSL, macOS with fontconfig) ──
|
|
98
|
+
try {
|
|
99
|
+
const out = execSync(`fc-match "${fontName}" 2>/dev/null`, {
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
maxBuffer: 1024,
|
|
102
|
+
shell: "/bin/sh",
|
|
103
|
+
}).trim();
|
|
104
|
+
if (out && !out.startsWith("fontconfig error")) {
|
|
105
|
+
const parts = out.split(":").slice(1).join(":").trim();
|
|
106
|
+
if (parts) return parts;
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
|
|
110
|
+
// ── Windows: PowerShell registry query ──
|
|
111
|
+
if (process.platform === "win32") {
|
|
112
|
+
try {
|
|
113
|
+
const safe = fontName.replace(/'/g, "''");
|
|
114
|
+
const ps = `$n='${safe}';$p=Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' 2>$null;foreach($v in $p.PSObject.Properties){if($v.Value -like"*$n*"){Write-Output $v.Value;exit 0}}`;
|
|
115
|
+
const out = execSync(
|
|
116
|
+
`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`,
|
|
117
|
+
{ encoding: "utf8", timeout: 5000 }
|
|
118
|
+
).trim();
|
|
119
|
+
if (out && out !== "NOT_FOUND") return out;
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── macOS: system_profiler (slow but reliable) ──
|
|
124
|
+
if (process.platform === "darwin") {
|
|
125
|
+
try {
|
|
126
|
+
const kw = fontName.toLowerCase();
|
|
127
|
+
const out = execSync(
|
|
128
|
+
`system_profiler SPFontsDataType 2>/dev/null | grep -i "${kw}"`,
|
|
129
|
+
{ encoding: "utf8", maxBuffer: 1024 * 1024, timeout: 10000 }
|
|
130
|
+
).trim();
|
|
131
|
+
if (out) return out.split("\n")[0].trim();
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Universal: scan known font directories ──
|
|
136
|
+
const fontDirs: string[] = [];
|
|
137
|
+
if (process.platform === "win32") {
|
|
138
|
+
const sysRoot = process.env.SystemRoot || "C:\\Windows";
|
|
139
|
+
fontDirs.push(join(sysRoot, "Fonts"));
|
|
140
|
+
} else if (process.platform === "darwin") {
|
|
141
|
+
const home = process.env.HOME || "";
|
|
142
|
+
fontDirs.push("/Library/Fonts", "/System/Library/Fonts");
|
|
143
|
+
if (home) fontDirs.push(join(home, "Library", "Fonts"));
|
|
144
|
+
} else {
|
|
145
|
+
// Linux / WSL
|
|
146
|
+
fontDirs.push(
|
|
147
|
+
"/usr/share/fonts",
|
|
148
|
+
"/usr/local/share/fonts",
|
|
149
|
+
);
|
|
150
|
+
const home = process.env.HOME || "";
|
|
151
|
+
if (home) fontDirs.push(join(home, ".local", "share", "fonts"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const kw = fontName.replace(/[()\[\]]/g, "").split(/\s+/).filter(Boolean);
|
|
155
|
+
if (kw.length === 0) return null;
|
|
156
|
+
|
|
157
|
+
for (const dir of fontDirs) {
|
|
158
|
+
if (!existsSync(dir)) continue;
|
|
159
|
+
try {
|
|
160
|
+
const files = readdirSync(dir, { recursive: true });
|
|
161
|
+
const found = files.find((f) => {
|
|
162
|
+
const lower = typeof f === "string" ? f.toLowerCase() : "";
|
|
163
|
+
return /\.(ttf|ttc|otf|woff2?)$/i.test(lower) && kw.every((k) => lower.includes(k.toLowerCase()));
|
|
164
|
+
});
|
|
165
|
+
if (found) return typeof found === "string" ? found : String(found);
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse unique font-family names from a CSS string.
|
|
174
|
+
* Extracts font names from all `font-family: ...` declarations,
|
|
175
|
+
* filters out CSS variable references (var(--...)).
|
|
176
|
+
*/
|
|
177
|
+
function parseFontsFromCSS(css: string): string[] {
|
|
178
|
+
const names = new Set<string>();
|
|
179
|
+
const isKeyword = /^(inherit|initial|unset|revert)$/i;
|
|
180
|
+
// Match all font-family declarations
|
|
181
|
+
const re = /font-family:\s*([^;{}]+)/gi;
|
|
182
|
+
let match: RegExpExecArray | null;
|
|
183
|
+
while ((match = re.exec(css)) !== null) {
|
|
184
|
+
const value = match[1];
|
|
185
|
+
// Split by comma, trim quotes, skip var() references
|
|
186
|
+
for (const part of value.split(",")) {
|
|
187
|
+
let name = part.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
188
|
+
// Strip !important, trailing semicolons
|
|
189
|
+
name = name.replace(/\s*!important\s*$/i, "").trim();
|
|
190
|
+
if (name && !name.startsWith("var(") && !isKeyword.test(name)) {
|
|
191
|
+
names.add(name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return Array.from(names);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let configCache: { themes: Record<string, string> } | null = null;
|
|
199
|
+
|
|
200
|
+
async function loadCustomThemes(): Promise<Record<string, string>> {
|
|
201
|
+
if (configCache) return configCache.themes;
|
|
202
|
+
try {
|
|
203
|
+
const { getConfig } = await import("./theme-store.js");
|
|
204
|
+
const config = await getConfig();
|
|
205
|
+
configCache = config;
|
|
206
|
+
return config.themes;
|
|
207
|
+
} catch {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const program = new Command();
|
|
213
|
+
|
|
214
|
+
program
|
|
215
|
+
.name("colamd-themes")
|
|
216
|
+
.description("ColaMD Theme Development Tool — extract formatting and generate 2.7-contract CSS themes")
|
|
217
|
+
.version(getPackageVersion());
|
|
218
|
+
|
|
219
|
+
// ── Shared extract options builder ──
|
|
220
|
+
|
|
221
|
+
function addExtractOptions(cmd: Command): Command {
|
|
222
|
+
return cmd
|
|
223
|
+
.option("-n, --name <name>", "Theme name")
|
|
224
|
+
.option("-o, --output <path>", "Output CSS file path")
|
|
225
|
+
.option("-d, --themes-dir <dir>", "Themes directory", "themes")
|
|
226
|
+
.option("-c, --color-system <id>", "Force a color system (use `color-systems` to list)")
|
|
227
|
+
.option("-A, --auto-match", "Auto-match a color system from the extracted palette");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runExtraction(
|
|
231
|
+
extractor: Extractor,
|
|
232
|
+
source: string,
|
|
233
|
+
options: Record<string, string>
|
|
234
|
+
) {
|
|
235
|
+
const spinner = ora("Extracting styles...").start();
|
|
236
|
+
try {
|
|
237
|
+
const style = await extractor.extract(source);
|
|
238
|
+
if (options.name) style.name = options.name;
|
|
239
|
+
if (options.colorSystem) {
|
|
240
|
+
const cs = getColorSystem(options.colorSystem);
|
|
241
|
+
if (!cs) { spinner.fail(`Unknown color system: ${options.colorSystem}`); process.exit(1); }
|
|
242
|
+
style.seed = { ...cs.palette };
|
|
243
|
+
style.colorSystemId = cs.id;
|
|
244
|
+
}
|
|
245
|
+
if (options.autoMatch) style.colorSystemId = "";
|
|
246
|
+
spinner.text = "Generating CSS (2.7.8-rc.1 contract)...";
|
|
247
|
+
const outputPath = writeThemeFile(style, options.output || options.themesDir);
|
|
248
|
+
spinner.succeed(chalk.green(`Theme "${style.name}" generated: ${outputPath}`));
|
|
249
|
+
printSummary(style);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
spinner.fail(chalk.red(`Failed: ${(err as Error).message}`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── from-url ──
|
|
257
|
+
addExtractOptions(
|
|
258
|
+
program.command("from-url")
|
|
259
|
+
.description("Extract theme from a web page URL")
|
|
260
|
+
.argument("<url>", "URL of the web page")
|
|
261
|
+
).action(async (url: string, options: Record<string, string>) => {
|
|
262
|
+
await runExtraction(new UrlExtractor(), url, options);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── from-docx ──
|
|
266
|
+
addExtractOptions(
|
|
267
|
+
program.command("from-docx")
|
|
268
|
+
.description("Extract theme from a .docx document")
|
|
269
|
+
.argument("<file>", "Path to the .docx file")
|
|
270
|
+
).action(async (file: string, options: Record<string, string>) => {
|
|
271
|
+
if (!existsSync(file)) { console.error(chalk.red(`File not found: ${file}`)); process.exit(1); }
|
|
272
|
+
await runExtraction(new DocxExtractor(), file, options);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── from-pdf ──
|
|
276
|
+
addExtractOptions(
|
|
277
|
+
program.command("from-pdf")
|
|
278
|
+
.description("Extract theme from a PDF document")
|
|
279
|
+
.argument("<file>", "Path to the PDF file")
|
|
280
|
+
).action(async (file: string, options: Record<string, string>) => {
|
|
281
|
+
if (!existsSync(file)) { console.error(chalk.red(`File not found: ${file}`)); process.exit(1); }
|
|
282
|
+
await runExtraction(new PdfExtractor(), file, options);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ── extract (auto-detect) ──
|
|
286
|
+
addExtractOptions(
|
|
287
|
+
program.command("extract")
|
|
288
|
+
.description("Auto-detect source type and extract theme")
|
|
289
|
+
.argument("<source>", "URL, .docx file, or .pdf file")
|
|
290
|
+
).action(async (source: string, options: Record<string, string>) => {
|
|
291
|
+
const extractors: Extractor[] = [new UrlExtractor(), new DocxExtractor(), new PdfExtractor()];
|
|
292
|
+
const ext = extractors.find((e) => e.canHandle(source));
|
|
293
|
+
if (!ext) { console.error(chalk.red(`Cannot detect source type for: ${source}`)); process.exit(1); }
|
|
294
|
+
await runExtraction(ext, source, options);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ── color-systems ──
|
|
298
|
+
program
|
|
299
|
+
.command("color-systems")
|
|
300
|
+
.description("List all 15 built-in color systems")
|
|
301
|
+
.option("-j, --json", "Output as JSON")
|
|
302
|
+
.action((options: Record<string, string>) => {
|
|
303
|
+
if (options.json) { console.log(JSON.stringify(COLOR_SYSTEMS, null, 2)); return; }
|
|
304
|
+
const families: Record<string, typeof COLOR_SYSTEMS> = {};
|
|
305
|
+
for (const cs of COLOR_SYSTEMS) {
|
|
306
|
+
const fam = cs.nameCN.split(" ")[0];
|
|
307
|
+
(families[fam] ??= []).push(cs);
|
|
308
|
+
}
|
|
309
|
+
for (const [fam, systems] of Object.entries(families)) {
|
|
310
|
+
console.log(chalk.bold(`\n${fam}`));
|
|
311
|
+
for (const cs of systems) {
|
|
312
|
+
console.log(` ${chalk.cyan(cs.id.padEnd(24))} ${chalk.white(cs.nameCN.split(" ").slice(1).join(" "))} ${chalk.dim(cs.palette.accent)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
console.log();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── list ──
|
|
319
|
+
program
|
|
320
|
+
.command("list")
|
|
321
|
+
.description("List all generated themes")
|
|
322
|
+
.option("-d, --themes-dir <dir>", "Themes directory", "themes")
|
|
323
|
+
.action((options: Record<string, string>) => {
|
|
324
|
+
const dirPath = isAbsolute(options.themesDir) ? options.themesDir : join(process.cwd(), options.themesDir);
|
|
325
|
+
if (!existsSync(dirPath)) { console.log(chalk.yellow(`No themes directory: ${dirPath}`)); return; }
|
|
326
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith(".css"));
|
|
327
|
+
if (files.length === 0) { console.log(chalk.yellow("No themes found.")); return; }
|
|
328
|
+
console.log(chalk.bold(`\nThemes in ${dirPath}/:`));
|
|
329
|
+
for (const f of files) console.log(` ${chalk.cyan("●")} ${chalk.white(f.replace(".css", ""))}`);
|
|
330
|
+
console.log(`\n${files.length} theme(s) total.\n`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ── validate ──
|
|
334
|
+
program
|
|
335
|
+
.command("validate")
|
|
336
|
+
.description("Validate a theme CSS against the 2.7.8-rc.1 contract and paradigm rules")
|
|
337
|
+
.argument("<file>", "Path to theme CSS file")
|
|
338
|
+
.action((file: string) => {
|
|
339
|
+
if (!existsSync(file)) { console.error(chalk.red(`File not found: ${file}`)); process.exit(1); }
|
|
340
|
+
const css = readFileSync(file, "utf-8");
|
|
341
|
+
const extract = (name: string) => {
|
|
342
|
+
const m = css.match(new RegExp(`${name}:\\s*([^;]+);`));
|
|
343
|
+
return m ? m[1].trim() : "";
|
|
344
|
+
};
|
|
345
|
+
const seed = {
|
|
346
|
+
accent: extract("--seed-accent"), accentLight: extract("--seed-accent-light"),
|
|
347
|
+
accentDark: extract("--seed-accent-dark"), surface: extract("--seed-surface"),
|
|
348
|
+
panel: extract("--seed-panel"), panelAlt: extract("--seed-panel-alt"),
|
|
349
|
+
ink: extract("--seed-ink"), inkMuted: extract("--seed-ink-muted"),
|
|
350
|
+
inkDim: extract("--seed-ink-dim"), border: extract("--seed-border"),
|
|
351
|
+
borderStrong: extract("--seed-border-strong"),
|
|
352
|
+
};
|
|
353
|
+
console.log(chalk.bold("CSS contract (ColaMD 2.7.8-rc.1):"));
|
|
354
|
+
const cssIssues = validateThemeCss(css);
|
|
355
|
+
console.log(cssIssues.length === 0 ? chalk.green(" ✔ :root 根块与选择器契约通过") : summarizeIssues(cssIssues));
|
|
356
|
+
|
|
357
|
+
if (!seed.accent) {
|
|
358
|
+
console.log(chalk.yellow("\nNot a seed-palette theme (no --seed-* found); seed checks skipped."));
|
|
359
|
+
if (cssIssues.some((i) => i.level === "MUST" || i.level === "MUST_NOT")) process.exit(1);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk.bold("\nSeed palette:"));
|
|
363
|
+
for (const [k, v] of Object.entries(seed)) console.log(` ${chalk.dim(k.padEnd(18))} ${v}`);
|
|
364
|
+
const issues = validateSeedPalette(seed);
|
|
365
|
+
console.log(chalk.bold(`\nParadigm validation (${issues.length} issues):`));
|
|
366
|
+
console.log(summarizeIssues(issues));
|
|
367
|
+
if (issues.filter((i) => i.level === "MUST" || i.level === "MUST_NOT").length > 0) process.exit(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ── check-fonts ──
|
|
371
|
+
program
|
|
372
|
+
.command("check-fonts")
|
|
373
|
+
.description("Check whether built-in/custom theme fonts are installed on this system")
|
|
374
|
+
.argument("[theme]", "Theme name (built-in, registered custom, or .css file path); omit to check all")
|
|
375
|
+
.action(async (theme?: string) => {
|
|
376
|
+
const customThemes = await loadCustomThemes();
|
|
377
|
+
let fontSources: { name: string; fonts: string[] }[] = [];
|
|
378
|
+
|
|
379
|
+
if (theme) {
|
|
380
|
+
// 1. Check if it's a built-in theme
|
|
381
|
+
if (BUILTIN_THEMES.includes(theme as any)) {
|
|
382
|
+
fontSources.push({ name: theme, fonts: THEME_FONTS[theme] });
|
|
383
|
+
}
|
|
384
|
+
// 2. Check if it's a registered custom theme
|
|
385
|
+
else if (customThemes[theme]) {
|
|
386
|
+
const css = readFileSync(customThemes[theme], "utf-8");
|
|
387
|
+
const fonts = parseFontsFromCSS(css);
|
|
388
|
+
if (fonts.length === 0) {
|
|
389
|
+
console.log(chalk.yellow(` 主题 "${theme}" 中未找到 font-family 声明`));
|
|
390
|
+
}
|
|
391
|
+
fontSources.push({ name: `${theme} (custom)`, fonts });
|
|
392
|
+
}
|
|
393
|
+
// 3. Treat as direct CSS file path
|
|
394
|
+
else if (existsSync(theme) && theme.endsWith(".css")) {
|
|
395
|
+
const css = readFileSync(theme, "utf-8");
|
|
396
|
+
const fonts = parseFontsFromCSS(css);
|
|
397
|
+
if (fonts.length === 0) {
|
|
398
|
+
console.log(chalk.yellow(` 文件 ${theme} 中未找到 font-family 声明`));
|
|
399
|
+
}
|
|
400
|
+
fontSources.push({ name: theme, fonts });
|
|
401
|
+
}
|
|
402
|
+
// 4. Unknown
|
|
403
|
+
else {
|
|
404
|
+
console.error(chalk.red(`Unknown theme: "${theme}".\n Built-in: ${BUILTIN_THEMES.join(", ")}\n Registered: ${Object.keys(customThemes).join(", ") || "(none)"}`));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
// No argument: built-in themes + all registered custom themes
|
|
409
|
+
for (const t of Object.keys(THEME_FONTS)) {
|
|
410
|
+
fontSources.push({ name: t, fonts: THEME_FONTS[t] });
|
|
411
|
+
}
|
|
412
|
+
for (const [name, cssPath] of Object.entries(customThemes)) {
|
|
413
|
+
const css = readFileSync(cssPath, "utf-8");
|
|
414
|
+
const fonts = parseFontsFromCSS(css);
|
|
415
|
+
if (fonts.length > 0) {
|
|
416
|
+
fontSources.push({ name: `${name} (custom)`, fonts });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log(chalk.bold("══ 主题字体检查 ══"));
|
|
422
|
+
let total = 0;
|
|
423
|
+
let missing = 0;
|
|
424
|
+
|
|
425
|
+
for (const { name, fonts } of fontSources) {
|
|
426
|
+
if (fonts.length === 0) continue;
|
|
427
|
+
console.log(`\n 主题:${chalk.cyan(name)}`);
|
|
428
|
+
for (const f of fonts) {
|
|
429
|
+
total++;
|
|
430
|
+
const result = checkFont(f);
|
|
431
|
+
if (result) {
|
|
432
|
+
console.log(` ${chalk.green("✔")} ${f} -> ${result}`);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(` ${chalk.red("✘")} ${f} -> 未找到`);
|
|
435
|
+
missing++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
console.log("");
|
|
441
|
+
if (missing > 0) {
|
|
442
|
+
console.log(`${chalk.yellow(`⚠ ${missing}/${total}`)} 个字体缺失,导出时可能回退到系统默认字体`);
|
|
443
|
+
console.log(" 建议安装:");
|
|
444
|
+
if (process.platform === "win32") {
|
|
445
|
+
console.log(" Windows: 从 https://github.com/lxgw/LxgwWenKai/releases 下载字体双击安装");
|
|
446
|
+
} else if (process.platform === "darwin") {
|
|
447
|
+
console.log(" brew install --cask font-lxgw-wenkai");
|
|
448
|
+
console.log(" brew install --cask font-noto-serif-cjk-sc");
|
|
449
|
+
} else {
|
|
450
|
+
console.log(" sudo apt-get install -y fonts-noto-cjk-extra # Linux/WSL");
|
|
451
|
+
}
|
|
452
|
+
process.exit(1);
|
|
453
|
+
} else {
|
|
454
|
+
console.log(chalk.green("✔ 所有主题字体检查通过"));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// ── Export commands (set-theme, export-html, export-pdf, export) ──
|
|
459
|
+
registerExportCommands(program);
|
|
460
|
+
|
|
461
|
+
// ── Summary ──
|
|
462
|
+
|
|
463
|
+
function printSummary(style: ThemeStyle): void {
|
|
464
|
+
const s = style.seed;
|
|
465
|
+
console.log(chalk.dim("\n Seed palette:"));
|
|
466
|
+
console.log(` ${chalk.white("surface:")} ${s.surface} ${chalk.white("ink:")} ${s.ink} ${chalk.white("accent:")} ${s.accent}`);
|
|
467
|
+
console.log(` ${chalk.white("panel:")} ${s.panel} ${chalk.white("panel-alt:")} ${s.panelAlt}`);
|
|
468
|
+
if (style.colorSystemId) {
|
|
469
|
+
console.log(chalk.dim(` Color system: ${style.colorSystemId}`));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function main(): void { program.parse(); }
|
|
474
|
+
main();
|