@bytechain.cn/colamd-themes 0.3.1 → 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.
- package/.openclaw/skills/colamd-themes/SKILL.md +15 -124
- package/.trae/rules/colamd-themes.md +15 -124
- package/CHANGELOG.md +4 -3
- package/README.md +1 -1
- package/README_CN.md +1 -1
- package/RELEASENOTE.md +24 -0
- package/dist/cli.js +260 -1
- package/dist/cli.js.map +1 -1
- package/dist/renderer.js +66 -3
- package/dist/renderer.js.map +1 -1
- package/package.json +1 -1
- package/skills/colamd-themes/SKILL.md +30 -0
- package/src/cli.ts +271 -1
- package/src/renderer.ts +74 -3
package/src/cli.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, isAbsolute, dirname } from "node:path";
|
|
5
|
+
import { join, isAbsolute, dirname, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import ora from "ora";
|
|
9
10
|
import { UrlExtractor } from "./extractors/url-extractor.js";
|
|
@@ -16,6 +17,7 @@ import { MERMAID_PRESETS } from "./mermaid-presets.js";
|
|
|
16
17
|
import { validateSeedPalette, summarizeIssues } from "./validators.js";
|
|
17
18
|
import type { ThemeStyle } from "./models.js";
|
|
18
19
|
import { registerExportCommands } from "./export-cli.js";
|
|
20
|
+
import { BUILTIN_THEMES } from "./theme-store.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Dynamically read version from package.json to avoid hardcoding.
|
|
@@ -33,6 +35,186 @@ function getPackageVersion(): string {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/** Font stacks for each built-in theme — parsed dynamically from colamd.css. */
|
|
39
|
+
const THEME_FONTS: Record<string, string[]> = getBuiltinFontStacks();
|
|
40
|
+
|
|
41
|
+
/** Find the path to @bytechain.cn/colamd/dist/lib/colamd.css */
|
|
42
|
+
function findColamdCssPath(): string {
|
|
43
|
+
const candidates = [
|
|
44
|
+
resolve(dirname(new URL(import.meta.url).pathname), "../node_modules/@bytechain.cn/colamd/dist/lib/colamd.css"),
|
|
45
|
+
resolve(process.cwd(), "node_modules/@bytechain.cn/colamd/dist/lib/colamd.css"),
|
|
46
|
+
];
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
if (existsSync(p)) return p;
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse font-family values for each built-in theme from colamd.css.
|
|
55
|
+
* Dynamic approach: reads `body.theme-{name} #editor .ProseMirror { font-family: ... }`
|
|
56
|
+
* instead of hardcoding font stacks.
|
|
57
|
+
*/
|
|
58
|
+
function getBuiltinFontStacks(): Record<string, string[]> {
|
|
59
|
+
const cssPath = findColamdCssPath();
|
|
60
|
+
if (!cssPath) {
|
|
61
|
+
// Fallback hardcoded defaults if colamd.css not found
|
|
62
|
+
return {
|
|
63
|
+
light: ["Noto Sans SC", "sans-serif"],
|
|
64
|
+
dark: ["Noto Sans SC", "sans-serif"],
|
|
65
|
+
elegant: ["LXGW WenKai", "Noto Serif SC", "serif"],
|
|
66
|
+
newsprint: ["Noto Serif SC", "serif"],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const css = readFileSync(cssPath, "utf-8");
|
|
71
|
+
const result: Record<string, string[]> = {};
|
|
72
|
+
const names = ["light", "dark", "elegant", "newsprint"];
|
|
73
|
+
|
|
74
|
+
for (const theme of names) {
|
|
75
|
+
// Match: body.theme-elegant #editor .ProseMirror { ... font-family: 'LXGW WenKai', ...; }
|
|
76
|
+
const re = new RegExp(
|
|
77
|
+
`body\\.theme-${theme}\\s+#editor\\s+\\.ProseMirror\\s*\\{[^}]*font-family:\\s*([^;]+)`,
|
|
78
|
+
"i"
|
|
79
|
+
);
|
|
80
|
+
const m = css.match(re);
|
|
81
|
+
if (m) {
|
|
82
|
+
const fonts = m[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "").trim());
|
|
83
|
+
result[theme] = fonts;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fill any missing themes with fallback
|
|
88
|
+
for (const theme of names) {
|
|
89
|
+
if (!result[theme]) {
|
|
90
|
+
result[theme] = ["sans-serif"];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a single font name is available on the system.
|
|
99
|
+
* Multi-platform: fc-match (Linux/macOS/WSL), PowerShell registry (Windows),
|
|
100
|
+
* system_profiler (macOS), universal directory scan fallback.
|
|
101
|
+
*/
|
|
102
|
+
function checkFont(fontName: string): string | null {
|
|
103
|
+
// ── try fc-match first (Linux, WSL, macOS with fontconfig) ──
|
|
104
|
+
try {
|
|
105
|
+
const out = execSync(`fc-match "${fontName}" 2>/dev/null`, {
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
maxBuffer: 1024,
|
|
108
|
+
shell: "/bin/sh",
|
|
109
|
+
}).trim();
|
|
110
|
+
if (out && !out.startsWith("fontconfig error")) {
|
|
111
|
+
const parts = out.split(":").slice(1).join(":").trim();
|
|
112
|
+
if (parts) return parts;
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
// ── Windows: PowerShell registry query ──
|
|
117
|
+
if (process.platform === "win32") {
|
|
118
|
+
try {
|
|
119
|
+
const safe = fontName.replace(/'/g, "''");
|
|
120
|
+
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}}`;
|
|
121
|
+
const out = execSync(
|
|
122
|
+
`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`,
|
|
123
|
+
{ encoding: "utf8", timeout: 5000 }
|
|
124
|
+
).trim();
|
|
125
|
+
if (out && out !== "NOT_FOUND") return out;
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── macOS: system_profiler (slow but reliable) ──
|
|
130
|
+
if (process.platform === "darwin") {
|
|
131
|
+
try {
|
|
132
|
+
const kw = fontName.toLowerCase();
|
|
133
|
+
const out = execSync(
|
|
134
|
+
`system_profiler SPFontsDataType 2>/dev/null | grep -i "${kw}"`,
|
|
135
|
+
{ encoding: "utf8", maxBuffer: 1024 * 1024, timeout: 10000 }
|
|
136
|
+
).trim();
|
|
137
|
+
if (out) return out.split("\n")[0].trim();
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Universal: scan known font directories ──
|
|
142
|
+
const fontDirs: string[] = [];
|
|
143
|
+
if (process.platform === "win32") {
|
|
144
|
+
const sysRoot = process.env.SystemRoot || "C:\\Windows";
|
|
145
|
+
fontDirs.push(join(sysRoot, "Fonts"));
|
|
146
|
+
} else if (process.platform === "darwin") {
|
|
147
|
+
const home = process.env.HOME || "";
|
|
148
|
+
fontDirs.push("/Library/Fonts", "/System/Library/Fonts");
|
|
149
|
+
if (home) fontDirs.push(join(home, "Library", "Fonts"));
|
|
150
|
+
} else {
|
|
151
|
+
// Linux / WSL
|
|
152
|
+
fontDirs.push(
|
|
153
|
+
"/usr/share/fonts",
|
|
154
|
+
"/usr/local/share/fonts",
|
|
155
|
+
);
|
|
156
|
+
const home = process.env.HOME || "";
|
|
157
|
+
if (home) fontDirs.push(join(home, ".local", "share", "fonts"));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const kw = fontName.replace(/[()\[\]]/g, "").split(/\s+/).filter(Boolean);
|
|
161
|
+
if (kw.length === 0) return null;
|
|
162
|
+
|
|
163
|
+
for (const dir of fontDirs) {
|
|
164
|
+
if (!existsSync(dir)) continue;
|
|
165
|
+
try {
|
|
166
|
+
const files = readdirSync(dir, { recursive: true });
|
|
167
|
+
const found = files.find((f) => {
|
|
168
|
+
const lower = typeof f === "string" ? f.toLowerCase() : "";
|
|
169
|
+
return /\.(ttf|ttc|otf|woff2?)$/i.test(lower) && kw.every((k) => lower.includes(k.toLowerCase()));
|
|
170
|
+
});
|
|
171
|
+
if (found) return typeof found === "string" ? found : String(found);
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse unique font-family names from a CSS string.
|
|
180
|
+
* Extracts font names from all `font-family: ...` declarations,
|
|
181
|
+
* filters out CSS variable references (var(--...)).
|
|
182
|
+
*/
|
|
183
|
+
function parseFontsFromCSS(css: string): string[] {
|
|
184
|
+
const names = new Set<string>();
|
|
185
|
+
const isKeyword = /^(inherit|initial|unset|revert)$/i;
|
|
186
|
+
// Match all font-family declarations
|
|
187
|
+
const re = /font-family:\s*([^;{}]+)/gi;
|
|
188
|
+
let match: RegExpExecArray | null;
|
|
189
|
+
while ((match = re.exec(css)) !== null) {
|
|
190
|
+
const value = match[1];
|
|
191
|
+
// Split by comma, trim quotes, skip var() references
|
|
192
|
+
for (const part of value.split(",")) {
|
|
193
|
+
let name = part.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
194
|
+
// Strip !important, trailing semicolons
|
|
195
|
+
name = name.replace(/\s*!important\s*$/i, "").trim();
|
|
196
|
+
if (name && !name.startsWith("var(") && !isKeyword.test(name)) {
|
|
197
|
+
names.add(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Array.from(names);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let configCache: { themes: Record<string, string> } | null = null;
|
|
205
|
+
|
|
206
|
+
async function loadCustomThemes(): Promise<Record<string, string>> {
|
|
207
|
+
if (configCache) return configCache.themes;
|
|
208
|
+
try {
|
|
209
|
+
const { getConfig } = await import("./theme-store.js");
|
|
210
|
+
const config = await getConfig();
|
|
211
|
+
configCache = config;
|
|
212
|
+
return config.themes;
|
|
213
|
+
} catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
36
218
|
const program = new Command();
|
|
37
219
|
|
|
38
220
|
program
|
|
@@ -198,6 +380,94 @@ program
|
|
|
198
380
|
if (issues.filter((i) => i.level === "MUST" || i.level === "MUST_NOT").length > 0) process.exit(1);
|
|
199
381
|
});
|
|
200
382
|
|
|
383
|
+
// ── check-fonts ──
|
|
384
|
+
program
|
|
385
|
+
.command("check-fonts")
|
|
386
|
+
.description("Check whether built-in/custom theme fonts are installed on this system")
|
|
387
|
+
.argument("[theme]", "Theme name (built-in, registered custom, or .css file path); omit to check all")
|
|
388
|
+
.action(async (theme?: string) => {
|
|
389
|
+
const customThemes = await loadCustomThemes();
|
|
390
|
+
let fontSources: { name: string; fonts: string[] }[] = [];
|
|
391
|
+
|
|
392
|
+
if (theme) {
|
|
393
|
+
// 1. Check if it's a built-in theme
|
|
394
|
+
if (BUILTIN_THEMES.includes(theme as any)) {
|
|
395
|
+
fontSources.push({ name: theme, fonts: THEME_FONTS[theme] });
|
|
396
|
+
}
|
|
397
|
+
// 2. Check if it's a registered custom theme
|
|
398
|
+
else if (customThemes[theme]) {
|
|
399
|
+
const css = readFileSync(customThemes[theme], "utf-8");
|
|
400
|
+
const fonts = parseFontsFromCSS(css);
|
|
401
|
+
if (fonts.length === 0) {
|
|
402
|
+
console.log(chalk.yellow(` 主题 "${theme}" 中未找到 font-family 声明`));
|
|
403
|
+
}
|
|
404
|
+
fontSources.push({ name: `${theme} (custom)`, fonts });
|
|
405
|
+
}
|
|
406
|
+
// 3. Treat as direct CSS file path
|
|
407
|
+
else if (existsSync(theme) && theme.endsWith(".css")) {
|
|
408
|
+
const css = readFileSync(theme, "utf-8");
|
|
409
|
+
const fonts = parseFontsFromCSS(css);
|
|
410
|
+
if (fonts.length === 0) {
|
|
411
|
+
console.log(chalk.yellow(` 文件 ${theme} 中未找到 font-family 声明`));
|
|
412
|
+
}
|
|
413
|
+
fontSources.push({ name: theme, fonts });
|
|
414
|
+
}
|
|
415
|
+
// 4. Unknown
|
|
416
|
+
else {
|
|
417
|
+
console.error(chalk.red(`Unknown theme: "${theme}".\n Built-in: ${BUILTIN_THEMES.join(", ")}\n Registered: ${Object.keys(customThemes).join(", ") || "(none)"}`));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
// No argument: built-in themes + all registered custom themes
|
|
422
|
+
for (const t of Object.keys(THEME_FONTS)) {
|
|
423
|
+
fontSources.push({ name: t, fonts: THEME_FONTS[t] });
|
|
424
|
+
}
|
|
425
|
+
for (const [name, cssPath] of Object.entries(customThemes)) {
|
|
426
|
+
const css = readFileSync(cssPath, "utf-8");
|
|
427
|
+
const fonts = parseFontsFromCSS(css);
|
|
428
|
+
if (fonts.length > 0) {
|
|
429
|
+
fontSources.push({ name: `${name} (custom)`, fonts });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log(chalk.bold("══ 主题字体检查 ══"));
|
|
435
|
+
let total = 0;
|
|
436
|
+
let missing = 0;
|
|
437
|
+
|
|
438
|
+
for (const { name, fonts } of fontSources) {
|
|
439
|
+
if (fonts.length === 0) continue;
|
|
440
|
+
console.log(`\n 主题:${chalk.cyan(name)}`);
|
|
441
|
+
for (const f of fonts) {
|
|
442
|
+
total++;
|
|
443
|
+
const result = checkFont(f);
|
|
444
|
+
if (result) {
|
|
445
|
+
console.log(` ${chalk.green("✔")} ${f} -> ${result}`);
|
|
446
|
+
} else {
|
|
447
|
+
console.log(` ${chalk.red("✘")} ${f} -> 未找到`);
|
|
448
|
+
missing++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
console.log("");
|
|
454
|
+
if (missing > 0) {
|
|
455
|
+
console.log(`${chalk.yellow(`⚠ ${missing}/${total}`)} 个字体缺失,导出时可能回退到系统默认字体`);
|
|
456
|
+
console.log(" 建议安装:");
|
|
457
|
+
if (process.platform === "win32") {
|
|
458
|
+
console.log(" Windows: 从 https://github.com/lxgw/LxgwWenKai/releases 下载字体双击安装");
|
|
459
|
+
} else if (process.platform === "darwin") {
|
|
460
|
+
console.log(" brew install --cask font-lxgw-wenkai");
|
|
461
|
+
console.log(" brew install --cask font-noto-serif-cjk-sc");
|
|
462
|
+
} else {
|
|
463
|
+
console.log(" sudo apt-get install -y fonts-noto-cjk-extra # Linux/WSL");
|
|
464
|
+
}
|
|
465
|
+
process.exit(1);
|
|
466
|
+
} else {
|
|
467
|
+
console.log(chalk.green("✔ 所有主题字体检查通过"));
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
201
471
|
// ── Export commands (set-theme, export-html, export-pdf, export) ──
|
|
202
472
|
registerExportCommands(program);
|
|
203
473
|
|
package/src/renderer.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import puppeteer, { type Browser, type Page } from "puppeteer";
|
|
16
16
|
import { createServer, type Server } from "node:http";
|
|
17
17
|
import { readFileSync, existsSync, createReadStream } from "node:fs";
|
|
18
|
-
import { resolve, dirname } from "node:path";
|
|
18
|
+
import { resolve, dirname, join } from "node:path";
|
|
19
19
|
|
|
20
20
|
// ── Types ──
|
|
21
21
|
|
|
@@ -69,6 +69,7 @@ function findRendererPath(): string {
|
|
|
69
69
|
const SHELL_HTML = `<!DOCTYPE html>
|
|
70
70
|
<html><head>
|
|
71
71
|
<meta charset="utf-8">
|
|
72
|
+
<link rel="stylesheet" href="/colamd.css">
|
|
72
73
|
<style>body{margin:0;padding:0}#editor{width:100%;min-height:100vh}</style>
|
|
73
74
|
</head><body><div id="editor"></div></body></html>`;
|
|
74
75
|
|
|
@@ -78,6 +79,7 @@ interface LocalServer {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function startLocalServer(rendererPath: string): Promise<LocalServer> {
|
|
82
|
+
const cssPath = join(dirname(rendererPath), "colamd.css");
|
|
81
83
|
return new Promise((resolveStart, reject) => {
|
|
82
84
|
const server: Server = createServer((req, res) => {
|
|
83
85
|
if (req.url === "/" || req.url === "/index.html") {
|
|
@@ -86,6 +88,9 @@ function startLocalServer(rendererPath: string): Promise<LocalServer> {
|
|
|
86
88
|
} else if (req.url === "/colamd-renderer.js") {
|
|
87
89
|
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
|
|
88
90
|
createReadStream(rendererPath).pipe(res);
|
|
91
|
+
} else if (req.url === "/colamd.css") {
|
|
92
|
+
res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
|
|
93
|
+
createReadStream(cssPath).pipe(res);
|
|
89
94
|
} else {
|
|
90
95
|
res.writeHead(404);
|
|
91
96
|
res.end();
|
|
@@ -106,6 +111,69 @@ function startLocalServer(rendererPath: string): Promise<LocalServer> {
|
|
|
106
111
|
});
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// ── Export CSS variable supplementation ──
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* CSS variables that colamd-renderer.js's buildExportHTML() may not include.
|
|
118
|
+
* Injected as a post-processing step so we don't need to patch node_modules.
|
|
119
|
+
*/
|
|
120
|
+
const EXTRA_CSS_VARIABLES: Record<string, string> = {
|
|
121
|
+
"--seed-accent": "var(--accent-color, #333)", // hardcoded — not in colamd.css
|
|
122
|
+
"--seed-accent-light": "var(--accent-light, #e0e0e0)", // hardcoded — not in colamd.css
|
|
123
|
+
"--seed-accent-dark": "var(--accent-dark, #111)", // hardcoded — not in colamd.css
|
|
124
|
+
"--seed-surface": "var(--bg-color, #fff)", // chains to colamd.css --bg-color
|
|
125
|
+
"--seed-panel": "var(--surface-color, #f5f5f5)", // hardcoded — not in colamd.css
|
|
126
|
+
"--seed-panel-alt": "var(--surface-alt, #eee)", // hardcoded — not in colamd.css
|
|
127
|
+
"--seed-ink": "var(--text-color, #333)", // chains to colamd.css --text-color
|
|
128
|
+
"--seed-ink-muted": "var(--text-muted, #888)", // chains to colamd.css --text-muted
|
|
129
|
+
"--seed-ink-dim": "var(--text-dim, #aaa)", // hardcoded — not in colamd.css
|
|
130
|
+
"--seed-border": "var(--border-color, #ddd)", // chains to colamd.css --border-color
|
|
131
|
+
"--seed-border-strong": "var(--table-border, #ccc)", // hardcoded — not in colamd.css
|
|
132
|
+
"--heading-color": "var(--text-color, #333)", // chains to colamd.css --text-color
|
|
133
|
+
"--code-color": "var(--code-block-text, #333)", // chains to colamd.css --code-block-text
|
|
134
|
+
"--hr-color": "var(--border-color, #ddd)", // chains to colamd.css --border-color
|
|
135
|
+
"--table-header-text": "var(--text-color, #333)", // chains to colamd.css --text-color
|
|
136
|
+
"--table-border": "var(--border-color, #ddd)", // chains to colamd.css --border-color
|
|
137
|
+
"--scrollbar-thumb": "var(--border-color, #ccc)", // chains to colamd.css --border-color
|
|
138
|
+
"--scrollbar-thumb-hover": "var(--text-muted, #999)", // chains to colamd.css --text-muted
|
|
139
|
+
"--content-width": "780px", // hardcoded — no colamd.css equivalent
|
|
140
|
+
"--line-height-body": "1.75", // hardcoded — no colamd.css equivalent
|
|
141
|
+
"--line-height-heading": "1.3", // hardcoded — no colamd.css equivalent
|
|
142
|
+
"--line-height-code": "1.6", // hardcoded — no colamd.css equivalent
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Supplement a buildExportHTML() result with extra CSS variables
|
|
147
|
+
* that colamd-renderer.js may not include. Only adds variables
|
|
148
|
+
* that are not already present in the HTML's <style> block.
|
|
149
|
+
*/
|
|
150
|
+
function supplementExportHTML(html: string): string {
|
|
151
|
+
// Find the existing CSS variables (--name: value; or --name:value;)
|
|
152
|
+
const existing = new Set<string>();
|
|
153
|
+
const varRe = /(--[\w-]+)\s*:/g;
|
|
154
|
+
let vm: RegExpExecArray | null;
|
|
155
|
+
while ((vm = varRe.exec(html)) !== null) {
|
|
156
|
+
existing.add(vm[1]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build style block for only missing variables
|
|
160
|
+
const missingLines: string[] = [];
|
|
161
|
+
for (const [name, fallback] of Object.entries(EXTRA_CSS_VARIABLES)) {
|
|
162
|
+
if (!existing.has(name)) {
|
|
163
|
+
missingLines.push(` ${name}: ${fallback};`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (missingLines.length === 0) return html;
|
|
168
|
+
|
|
169
|
+
// Inject into :root block, or before first style rule
|
|
170
|
+
const supplement = `\n /* colamd-themes supplement */\n${missingLines.join("\n")}\n`;
|
|
171
|
+
return html.replace(
|
|
172
|
+
/(<style[^>]*>)/i,
|
|
173
|
+
(match, styleTag) => `${styleTag}:root {\n${supplement}}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
109
177
|
// ── Renderer ──
|
|
110
178
|
|
|
111
179
|
export class ColamdRenderer {
|
|
@@ -199,10 +267,13 @@ export class ColamdRenderer {
|
|
|
199
267
|
return (window as any).__colamdHandle.buildExportHTML();
|
|
200
268
|
});
|
|
201
269
|
|
|
270
|
+
// Post-process: supplement any CSS variables that colamd-renderer.js may not include
|
|
271
|
+
const supplementedHtml = supplementExportHTML(html);
|
|
272
|
+
|
|
202
273
|
// Inject custom title if provided
|
|
203
274
|
const finalHtml = input.title
|
|
204
|
-
?
|
|
205
|
-
:
|
|
275
|
+
? supplementedHtml.replace(/<title>[^<]*<\/title>/, `<title>${input.title}</title>`)
|
|
276
|
+
: supplementedHtml;
|
|
206
277
|
|
|
207
278
|
return { html: finalHtml };
|
|
208
279
|
}
|