@bytechain.cn/colamd-themes 0.5.1 → 0.5.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/README.md +25 -11
- package/cordis.patch.yml +9 -0
- package/dist/cli.js +3 -3
- package/dist/dsh-plugin.d.ts +44 -0
- package/dist/dsh-plugin.js +72 -0
- package/dist/dsh-plugin.js.map +1 -0
- package/dist/generator.d.ts +1 -1
- package/dist/generator.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +1 -1
- package/dist/models.js +1 -1
- package/dist/renderer.d.ts +2 -2
- package/dist/renderer.js +3 -3
- package/package.json +22 -2
- package/skills/build-skills.sh +5 -5
- package/skills/colamd-themes/SKILL.md +5 -5
- package/.claude/skills/colamd-themes-preview.skill.md +0 -333
- package/.claude/skills/colamd-themes.skill.md +0 -10
- package/.dsh/skills/colamd-themes/SKILL.md +0 -14
- package/.github/supported-topics.yml +0 -17
- package/.hermes/skills/tools/colamd-themes/SKILL.md +0 -14
- package/.openclaw/skills/colamd-themes/SKILL.md +0 -21
- package/.opencode/skills/colamd-themes/SKILL.md +0 -17
- package/.trae/rules/colamd-themes.md +0 -5
- package/CHANGELOG.md +0 -331
- package/DSH_SUPPORT_SUMMARY.md +0 -87
- package/README_CN.md +0 -412
- package/RELEASENOTE.md +0 -541
- package/mcp-server/dist/index.d.ts +0 -13
- package/mcp-server/dist/index.d.ts.map +0 -1
- package/mcp-server/dist/index.js.map +0 -1
- package/mcp-server/package-lock.json +0 -1759
- package/mcp-server/package.json +0 -24
- package/mcp-server/src/index.ts +0 -0
- package/mcp-server/tsconfig.json +0 -19
- package/src/cli.ts +0 -474
- package/src/color-systems.ts +0 -289
- package/src/color-utils.ts +0 -137
- package/src/contrast.ts +0 -66
- package/src/docx-style-paradigm.ts +0 -811
- package/src/export-cli.ts +0 -356
- package/src/extractors/base.ts +0 -8
- package/src/extractors/docx-extractor.ts +0 -417
- package/src/extractors/pdf-extractor.ts +0 -533
- package/src/extractors/url-extractor.ts +0 -846
- package/src/generator.ts +0 -197
- package/src/index.ts +0 -29
- package/src/models.ts +0 -212
- package/src/renderer.ts +0 -310
- package/src/templates/template.css +0 -220
- package/src/templates/theme-paradigm.md +0 -530
- package/src/templates/theme-template.hbs +0 -256
- package/src/theme-store.ts +0 -185
- package/src/types/pdf-parse.d.ts +0 -12
- package/src/utils/cli-utils.ts +0 -84
- package/src/utils/document-paradigm.ts +0 -118
- package/src/validators.ts +0 -298
- package/themes-preview/demo-content.md +0 -187
- package/tsconfig.json +0 -18
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web 页面样式提取器 — v2.0
|
|
3
|
-
*
|
|
4
|
-
* 增强功能:
|
|
5
|
-
* 1. 从 CSS 规则和内联样式中提取完整的元素样式定义
|
|
6
|
-
* 2. 提取各级标题(H1-H6)的字体、字号、颜色、行高
|
|
7
|
-
* 3. 提取正文字体、字号、行间距、字间距
|
|
8
|
-
* 4. 提取表格(表头/单元格)的完整样式
|
|
9
|
-
* 5. 检测代码块、引用块等特殊元素样式
|
|
10
|
-
* 6. 集成 DocxStyleParadigm 标准参考值填充缺失字段
|
|
11
|
-
* 7. 自动验证 WCAG 对比度合规性
|
|
12
|
-
*
|
|
13
|
-
* 技术优势(相比 PDF):
|
|
14
|
-
* - 可直接从 <style> 标签和外部 CSS 中解析规则级样式
|
|
15
|
-
* - 可从 DOM 元素的 style 属性获取内联样式
|
|
16
|
-
* - 可通过 cheerio 获取元素的 computed-style 近似值
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import axios from "axios";
|
|
20
|
-
import * as cheerio from "cheerio";
|
|
21
|
-
import { parse, walk, generate } from "css-tree";
|
|
22
|
-
import type { Extractor } from "./base.js";
|
|
23
|
-
import {
|
|
24
|
-
type ThemeStyle,
|
|
25
|
-
makeThemeStyle,
|
|
26
|
-
type ColorPalette,
|
|
27
|
-
} from "../models.js";
|
|
28
|
-
import { normalizeThemeStyle } from "../color-utils.js";
|
|
29
|
-
import {
|
|
30
|
-
ACADEMIC_PAPER_DEFAULTS,
|
|
31
|
-
getRecommendedPreset,
|
|
32
|
-
validateDocumentStyle,
|
|
33
|
-
} from "../docx-style-paradigm.js";
|
|
34
|
-
import { themeToParadigm, logExtractorWarnings } from "../utils/document-paradigm.js";
|
|
35
|
-
|
|
36
|
-
/** 解析后的 CSS 规则集合 */
|
|
37
|
-
type CssRuleMap = Map<string, Map<string, string>>;
|
|
38
|
-
|
|
39
|
-
/** 单个选择器匹配到的样式属性 */
|
|
40
|
-
interface ElementStyles {
|
|
41
|
-
fontFamily?: string;
|
|
42
|
-
fontSize?: string;
|
|
43
|
-
fontWeight?: string;
|
|
44
|
-
color?: string;
|
|
45
|
-
lineHeight?: string;
|
|
46
|
-
backgroundColor?: string;
|
|
47
|
-
borderColor?: string;
|
|
48
|
-
borderLeftColor?: string;
|
|
49
|
-
letterSpacing?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class UrlExtractor implements Extractor {
|
|
53
|
-
canHandle(source: string): boolean {
|
|
54
|
-
return /^https?:\/\//i.test(source);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Validate URL to prevent SSRF attacks and ensure security.
|
|
59
|
-
* - Only allows http/https protocols
|
|
60
|
-
* - Blocks private network addresses (localhost, 10.x, 172.16-31.x, 192.168.x, 169.254.x)
|
|
61
|
-
*/
|
|
62
|
-
private validateUrl(url: string): void {
|
|
63
|
-
let parsed: URL;
|
|
64
|
-
try {
|
|
65
|
-
parsed = new URL(url);
|
|
66
|
-
} catch {
|
|
67
|
-
throw new Error(`Invalid URL format: ${url}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Protocol whitelist
|
|
71
|
-
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
72
|
-
throw new Error(`Unsupported protocol "${parsed.protocol}". Only HTTP and HTTPS are allowed.`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Block private network ranges to prevent SSRF
|
|
76
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
77
|
-
const isPrivate =
|
|
78
|
-
hostname === "localhost" ||
|
|
79
|
-
hostname === "0.0.0.0" ||
|
|
80
|
-
/^127\./.test(hostname) ||
|
|
81
|
-
hostname === "::1" ||
|
|
82
|
-
hostname === "[::1]" ||
|
|
83
|
-
hostname === "::" ||
|
|
84
|
-
/^10\./.test(hostname) ||
|
|
85
|
-
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
|
|
86
|
-
/^192\.168\./.test(hostname) ||
|
|
87
|
-
/^169\.254\./.test(hostname) ||
|
|
88
|
-
/^100\.(6[4-9]|7\d|8\d|9\d|1[01]\d|12[0-7])\./.test(hostname) ||
|
|
89
|
-
/^\[?fc/.test(hostname) ||
|
|
90
|
-
/^fe[89ab]/.test(hostname);
|
|
91
|
-
|
|
92
|
-
if (isPrivate) {
|
|
93
|
-
throw new Error(`Private network access denied: ${hostname}. Only public URLs are allowed for security reasons.`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async extract(source: string): Promise<ThemeStyle> {
|
|
98
|
-
this.validateUrl(source);
|
|
99
|
-
|
|
100
|
-
const response = await axios.get(source, {
|
|
101
|
-
headers: { "User-Agent": "Mozilla/5.0 (compatible; ColaMD-Themes/1.0)" },
|
|
102
|
-
timeout: 15000,
|
|
103
|
-
maxRedirects: 3,
|
|
104
|
-
maxContentLength: 10 * 1024 * 1024,
|
|
105
|
-
responseType: "text",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const contentType = String(response.headers["content-type"] || "");
|
|
109
|
-
if (!contentType.includes("text/html") && !contentType.includes("application/xhtml")) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`URL 返回非 HTML 内容 (${contentType}),请使用对应的文件类型命令`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const html = typeof response.data === "string" ? response.data : String(response.data);
|
|
116
|
-
const $ = cheerio.load(html);
|
|
117
|
-
const theme = makeThemeStyle(this.detectName($, source), "url", source);
|
|
118
|
-
|
|
119
|
-
const styleCss = this.extractAllStyles($);
|
|
120
|
-
const cssRules = this.parseCssRules(styleCss);
|
|
121
|
-
|
|
122
|
-
this.extractBodyStyles(theme, cssRules, $);
|
|
123
|
-
this.extractHeadingStyles(theme, cssRules, $);
|
|
124
|
-
this.extractTableStyles(theme, cssRules, $);
|
|
125
|
-
this.extractCodeStyles(theme, cssRules, $);
|
|
126
|
-
this.extractBlockquoteStyles(theme, cssRules, $);
|
|
127
|
-
this.extractLinkAndAccent(theme, cssRules, $);
|
|
128
|
-
this.inferDocumentCharacteristics(theme, $);
|
|
129
|
-
|
|
130
|
-
this.applyParadigmDefaults(theme);
|
|
131
|
-
normalizeThemeStyle(theme);
|
|
132
|
-
return theme;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 从页面标题或 URL 推断主题名称
|
|
137
|
-
*/
|
|
138
|
-
private detectName($: cheerio.CheerioAPI, url: string): string {
|
|
139
|
-
const title = $("title").text().trim();
|
|
140
|
-
if (title) {
|
|
141
|
-
return title
|
|
142
|
-
.replace(/\s+/g, "-")
|
|
143
|
-
.replace(/[|:/\\.<>?*]/g, "-")
|
|
144
|
-
.replace(/-+/g, "-")
|
|
145
|
-
.slice(0, 60);
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
const hostname = new URL(url).hostname.replace(/^www\./, "");
|
|
149
|
-
return hostname.split(".")[0] || "web-theme";
|
|
150
|
-
} catch {
|
|
151
|
-
return "web-theme";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 提取所有 <style> 标签内的 CSS 文本
|
|
157
|
-
*/
|
|
158
|
-
private extractAllStyles($: cheerio.CheerioAPI): string {
|
|
159
|
-
const parts: string[] = [];
|
|
160
|
-
$("style").each((_, el) => {
|
|
161
|
-
const text = $(el).text().trim();
|
|
162
|
-
if (text && !text.includes("@font-face")) {
|
|
163
|
-
parts.push(text);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
return parts.join("\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 使用 css-tree 将 CSS 文本解析为结构化规则映射
|
|
171
|
-
*/
|
|
172
|
-
private parseCssRules(css: string): CssRuleMap {
|
|
173
|
-
const result = new Map<string, Map<string, string>>();
|
|
174
|
-
try {
|
|
175
|
-
const ast = parse(css, {
|
|
176
|
-
parseAtrulePrelude: false,
|
|
177
|
-
parseRulePrelude: false,
|
|
178
|
-
});
|
|
179
|
-
walk(ast, {
|
|
180
|
-
visit: "Rule",
|
|
181
|
-
enter(node) {
|
|
182
|
-
const prelude = generate(node.prelude).trim();
|
|
183
|
-
const props = new Map<string, string>();
|
|
184
|
-
walk(node.block, {
|
|
185
|
-
visit: "Declaration",
|
|
186
|
-
enter(decl) {
|
|
187
|
-
props.set(decl.property.toLowerCase(), generate(decl.value));
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
if (props.size > 0) {
|
|
191
|
-
result.set(prelude, props);
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
} catch {
|
|
196
|
-
// CSS 解析错误不阻断流程
|
|
197
|
-
}
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 提取正文(body/html)完整样式
|
|
203
|
-
*/
|
|
204
|
-
private extractBodyStyles(
|
|
205
|
-
theme: ThemeStyle,
|
|
206
|
-
rules: CssRuleMap,
|
|
207
|
-
$: cheerio.CheerioAPI
|
|
208
|
-
): void {
|
|
209
|
-
const p = theme.palette;
|
|
210
|
-
const t = theme.typography;
|
|
211
|
-
const st = theme.seedTypography;
|
|
212
|
-
|
|
213
|
-
const bodyStyles = this.resolveElementStyles(rules, $, [
|
|
214
|
-
"body",
|
|
215
|
-
"html",
|
|
216
|
-
":root",
|
|
217
|
-
"#app",
|
|
218
|
-
"#root",
|
|
219
|
-
".container",
|
|
220
|
-
"main",
|
|
221
|
-
]);
|
|
222
|
-
|
|
223
|
-
if (bodyStyles.fontFamily) {
|
|
224
|
-
t.bodyFontFamily = this.normalizeFontFamily(bodyStyles.fontFamily);
|
|
225
|
-
st.fontBody = t.bodyFontFamily;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (bodyStyles.fontSize) {
|
|
229
|
-
t.baseFontSize = this.normalizeFontSize(bodyStyles.fontSize);
|
|
230
|
-
st.fontSizeRoot = t.baseFontSize;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (bodyStyles.color) {
|
|
234
|
-
p.textColor = this.normalizeColor(bodyStyles.color);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (bodyStyles.backgroundColor) {
|
|
238
|
-
p.bgColor = this.normalizeColor(bodyStyles.backgroundColor);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (bodyStyles.lineHeight) {
|
|
242
|
-
const lh = parseFloat(bodyStyles.lineHeight);
|
|
243
|
-
if (lh >= 1.2 && lh <= 2.5) {
|
|
244
|
-
t.baseLineHeight = lh;
|
|
245
|
-
st.lineHeightBody = lh;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 从 body 标签的内联样式或属性补充
|
|
250
|
-
const bodyTag = $("body");
|
|
251
|
-
const inlineBg = bodyTag.attr("style")?.match(/background(?:-color)?:\s*([^;]+)/i)?.[1];
|
|
252
|
-
if (inlineBg && !p.bgColor) {
|
|
253
|
-
p.bgColor = this.normalizeColor(inlineBg);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 提取 H1-H6 各级标题的完整样式定义
|
|
259
|
-
*/
|
|
260
|
-
private extractHeadingStyles(
|
|
261
|
-
theme: ThemeStyle,
|
|
262
|
-
rules: CssRuleMap,
|
|
263
|
-
$: cheerio.CheerioAPI
|
|
264
|
-
): void {
|
|
265
|
-
const headingLevels = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
266
|
-
const st = theme.seedTypography;
|
|
267
|
-
const rootSize =
|
|
268
|
-
parseFloat(theme.typography.baseFontSize) || 16;
|
|
269
|
-
|
|
270
|
-
for (const level of headingLevels) {
|
|
271
|
-
const styles = this.resolveElementStyles(rules, $, [level]);
|
|
272
|
-
|
|
273
|
-
if (styles.fontSize) {
|
|
274
|
-
const pxSize = this.normalizeFontSizeToPx(styles.fontSize, rootSize);
|
|
275
|
-
const scale = pxSize / rootSize;
|
|
276
|
-
|
|
277
|
-
switch (level) {
|
|
278
|
-
case "h1":
|
|
279
|
-
st.fontScaleH1 = Math.max(st.fontScaleH1, scale);
|
|
280
|
-
break;
|
|
281
|
-
case "h2":
|
|
282
|
-
st.fontScaleH2 = Math.max(st.fontScaleH2, scale);
|
|
283
|
-
break;
|
|
284
|
-
case "h3":
|
|
285
|
-
st.fontScaleH3 = Math.max(st.fontScaleH3, scale);
|
|
286
|
-
break;
|
|
287
|
-
case "h4":
|
|
288
|
-
st.fontScaleH4 = Math.max(st.fontScaleH4, scale);
|
|
289
|
-
break;
|
|
290
|
-
case "h5":
|
|
291
|
-
st.fontScaleH5 = Math.max(st.fontScaleH5, scale);
|
|
292
|
-
break;
|
|
293
|
-
case "h6":
|
|
294
|
-
st.fontScaleH6 = Math.max(st.fontScaleH6, scale);
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (styles.fontWeight) {
|
|
300
|
-
const weight = parseInt(styles.fontWeight) || 400;
|
|
301
|
-
if (weight > theme.typography.headingWeight) {
|
|
302
|
-
theme.typography.headingWeight = Math.min(weight, 900);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (styles.color && level === "h1") {
|
|
307
|
-
theme.palette.headingColor = this.normalizeColor(styles.color);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (styles.fontFamily && level === "h1") {
|
|
311
|
-
theme.typography.headingFontFamily = this.normalizeFontFamily(
|
|
312
|
-
styles.fontFamily
|
|
313
|
-
);
|
|
314
|
-
st.fontHeading = theme.typography.headingFontFamily;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (styles.lineHeight && level === "h1") {
|
|
318
|
-
const lh = parseFloat(styles.lineHeight);
|
|
319
|
-
if (lh >= 1.1 && lh <= 1.6) {
|
|
320
|
-
st.lineHeightHeading = lh;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 如果没有提取到标题字体,使用正文字体族
|
|
326
|
-
if (!theme.typography.headingFontFamily) {
|
|
327
|
-
theme.typography.headingFontFamily = theme.typography.bodyFontFamily;
|
|
328
|
-
st.fontHeading = st.fontBody;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* 提取表格(th/td)完整样式
|
|
334
|
-
*/
|
|
335
|
-
private extractTableStyles(
|
|
336
|
-
theme: ThemeStyle,
|
|
337
|
-
rules: CssRuleMap,
|
|
338
|
-
$: cheerio.CheerioAPI
|
|
339
|
-
): void {
|
|
340
|
-
const p = theme.palette;
|
|
341
|
-
|
|
342
|
-
const thStyles = this.resolveElementStyles(rules, $, [
|
|
343
|
-
"th",
|
|
344
|
-
"thead th",
|
|
345
|
-
"table th",
|
|
346
|
-
".table th",
|
|
347
|
-
]);
|
|
348
|
-
|
|
349
|
-
if (thStyles.backgroundColor) {
|
|
350
|
-
p.tableHeaderBg = this.normalizeColor(thStyles.backgroundColor);
|
|
351
|
-
}
|
|
352
|
-
if (thStyles.color) {
|
|
353
|
-
// 表头文字色暂存到 headingColor 备用
|
|
354
|
-
if (!p.headingColor) p.headingColor = this.normalizeColor(thStyles.color);
|
|
355
|
-
}
|
|
356
|
-
if (thStyles.borderColor) {
|
|
357
|
-
p.borderColor = this.normalizeColor(thStyles.borderColor);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const tdStyles = this.resolveElementStyles(rules, $, [
|
|
361
|
-
"td",
|
|
362
|
-
"tbody td",
|
|
363
|
-
"table td",
|
|
364
|
-
".table td",
|
|
365
|
-
]);
|
|
366
|
-
|
|
367
|
-
if (tdStyles.backgroundColor) {
|
|
368
|
-
p.codeBg = this.normalizeColor(tdStyles.backgroundColor);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const tableStyles = this.resolveElementStyles(rules, $, [
|
|
372
|
-
"table",
|
|
373
|
-
".table",
|
|
374
|
-
]);
|
|
375
|
-
|
|
376
|
-
if (tableStyles.borderColor) {
|
|
377
|
-
p.borderColor = p.borderColor || this.normalizeColor(tableStyles.borderColor);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// 检查页面是否包含表格元素
|
|
381
|
-
if ($("table").length > 0 && !p.tableHeaderBg) {
|
|
382
|
-
p.tableHeaderBg = ACADEMIC_PAPER_DEFAULTS.table.headerBgColor;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* 提取代码块和内联代码样式
|
|
388
|
-
*/
|
|
389
|
-
private extractCodeStyles(
|
|
390
|
-
theme: ThemeStyle,
|
|
391
|
-
rules: CssRuleMap,
|
|
392
|
-
$: cheerio.CheerioAPI
|
|
393
|
-
): void {
|
|
394
|
-
const p = theme.palette;
|
|
395
|
-
|
|
396
|
-
const preStyles = this.resolveElementStyles(rules, $, [
|
|
397
|
-
"pre",
|
|
398
|
-
"code pre",
|
|
399
|
-
"pre code",
|
|
400
|
-
".code-block",
|
|
401
|
-
"pre[class]",
|
|
402
|
-
]);
|
|
403
|
-
|
|
404
|
-
if (preStyles.backgroundColor) {
|
|
405
|
-
p.codeBlockBg = this.normalizeColor(preStyles.backgroundColor);
|
|
406
|
-
}
|
|
407
|
-
if (preStyles.color) {
|
|
408
|
-
p.codeBlockText = this.normalizeColor(preStyles.color);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const codeStyles = this.resolveElementStyles(rules, $, [
|
|
412
|
-
"code",
|
|
413
|
-
"p code",
|
|
414
|
-
"li code",
|
|
415
|
-
"inline-code",
|
|
416
|
-
":not(pre) > code",
|
|
417
|
-
]);
|
|
418
|
-
|
|
419
|
-
if (codeStyles.backgroundColor) {
|
|
420
|
-
p.codeBg = this.normalizeColor(codeStyles.backgroundColor);
|
|
421
|
-
}
|
|
422
|
-
if (codeStyles.color) {
|
|
423
|
-
// 内联代码颜色暂存
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 检测代码高亮库的存在
|
|
427
|
-
const hasHighlightJs = $("pre code.hljs").length > 0;
|
|
428
|
-
const hasPrismjs = $("pre[class*=language-]").length > 0;
|
|
429
|
-
if (hasHighlightJs || hasPrismjs) {
|
|
430
|
-
if (!p.codeBlockBg) p.codeBlockBg = "#282c34";
|
|
431
|
-
if (!p.codeBlockText) p.codeBlockText = "#abb2bf";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* 提取引用块样式
|
|
437
|
-
*/
|
|
438
|
-
private extractBlockquoteStyles(
|
|
439
|
-
theme: ThemeStyle,
|
|
440
|
-
rules: CssRuleMap,
|
|
441
|
-
$: cheerio.CheerioAPI
|
|
442
|
-
): void {
|
|
443
|
-
const p = theme.palette;
|
|
444
|
-
|
|
445
|
-
const bqStyles = this.resolveElementStyles(rules, $, [
|
|
446
|
-
"blockquote",
|
|
447
|
-
"blockquote p",
|
|
448
|
-
".blockquote",
|
|
449
|
-
]);
|
|
450
|
-
|
|
451
|
-
if (bqStyles.backgroundColor) {
|
|
452
|
-
p.blockquoteBg = this.normalizeColor(bqStyles.backgroundColor);
|
|
453
|
-
}
|
|
454
|
-
if (bqStyles.borderLeftColor) {
|
|
455
|
-
p.blockquoteBorder = this.normalizeColor(bqStyles.borderLeftColor);
|
|
456
|
-
} else if (bqStyles.borderColor) {
|
|
457
|
-
p.blockquoteBorder = this.normalizeColor(bqStyles.borderColor);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* 提取链接和强调色
|
|
463
|
-
*/
|
|
464
|
-
private extractLinkAndAccent(
|
|
465
|
-
theme: ThemeStyle,
|
|
466
|
-
rules: CssRuleMap,
|
|
467
|
-
$: cheerio.CheerioAPI
|
|
468
|
-
): void {
|
|
469
|
-
const p = theme.palette;
|
|
470
|
-
|
|
471
|
-
const linkStyles = this.resolveElementStyles(rules, $, [
|
|
472
|
-
"a",
|
|
473
|
-
"a:link",
|
|
474
|
-
"a:not(:visited)",
|
|
475
|
-
]);
|
|
476
|
-
|
|
477
|
-
if (linkStyles.color) {
|
|
478
|
-
p.accentColor = this.normalizeColor(linkStyles.color);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// 也检查 strong/b 元素的颜色
|
|
482
|
-
const strongStyles = this.resolveElementStyles(rules, $, [
|
|
483
|
-
"strong",
|
|
484
|
-
"b",
|
|
485
|
-
".strong",
|
|
486
|
-
]);
|
|
487
|
-
|
|
488
|
-
if (strongStyles.color && !p.accentColor) {
|
|
489
|
-
p.accentColor = this.normalizeColor(strongStyles.color);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* 综合推断文档特征(语言、类型、风格)
|
|
495
|
-
*/
|
|
496
|
-
private inferDocumentCharacteristics(
|
|
497
|
-
theme: ThemeStyle,
|
|
498
|
-
$: cheerio.CheerioAPI
|
|
499
|
-
): void {
|
|
500
|
-
const htmlText = $("body").text();
|
|
501
|
-
const hasChinese = /[\u4e00-\u9fa5]/.test(htmlText.slice(0, 2000));
|
|
502
|
-
|
|
503
|
-
// 检测常见框架/站点特征
|
|
504
|
-
const isGitHub = /github\.com/i.test(theme.sourceUrl);
|
|
505
|
-
const isDocsSite =
|
|
506
|
-
/docs?|documentation|wiki|help/i.test(theme.sourceUrl + $("title").text());
|
|
507
|
-
const isBlog = /blog|article|post/i.test(
|
|
508
|
-
theme.sourceUrl + $("title").text()
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
if (isGitHub) {
|
|
512
|
-
theme.description = `${theme.description} [GitHub 页面]`;
|
|
513
|
-
} else if (isDocsSite) {
|
|
514
|
-
theme.description = `${theme.description} [技术文档站]`;
|
|
515
|
-
} else if (isBlog) {
|
|
516
|
-
theme.description = `${theme.description} [博客/文章页]`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// 根据语言特征微调默认配色
|
|
520
|
-
if (hasChinese && !theme.palette.textColor) {
|
|
521
|
-
theme.typography.bodyFontFamily =
|
|
522
|
-
ACADEMIC_PAPER_DEFAULTS.body.fontFamily;
|
|
523
|
-
theme.seedTypography.fontBody = ACADEMIC_PAPER_DEFAULTS.body.fontFamily;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* 应用 DocxStyleParadigm 标准参考值填充缺失字段
|
|
529
|
-
*/
|
|
530
|
-
private applyParadigmDefaults(theme: ThemeStyle): void {
|
|
531
|
-
const preset = getRecommendedPreset();
|
|
532
|
-
const p = theme.palette;
|
|
533
|
-
const t = theme.typography;
|
|
534
|
-
const st = theme.seedTypography;
|
|
535
|
-
|
|
536
|
-
if (!p.textColor || p.textColor === "#000000") {
|
|
537
|
-
p.textColor = preset.body.color;
|
|
538
|
-
}
|
|
539
|
-
if (!p.headingColor) {
|
|
540
|
-
p.headingColor = preset.headings.h1.color;
|
|
541
|
-
}
|
|
542
|
-
if (!p.bgColor || p.bgColor === "#ffffff" || p.bgColor === "#fff") {
|
|
543
|
-
p.bgColor = preset.pageBackground;
|
|
544
|
-
}
|
|
545
|
-
if (!p.textMuted) {
|
|
546
|
-
p.textMuted = preset.mutedTextColor;
|
|
547
|
-
}
|
|
548
|
-
if (!p.accentColor || p.accentColor === "#0000ff" || p.accentColor === "#00f") {
|
|
549
|
-
p.accentColor = preset.accentColor;
|
|
550
|
-
}
|
|
551
|
-
if (!p.tableHeaderBg) {
|
|
552
|
-
p.tableHeaderBg = preset.table.headerBgColor;
|
|
553
|
-
}
|
|
554
|
-
if (!p.codeBg) {
|
|
555
|
-
p.codeBg = preset.inlineCode.bgColor;
|
|
556
|
-
}
|
|
557
|
-
if (!p.codeBlockBg) {
|
|
558
|
-
p.codeBlockBg = preset.codeBlock.bgColor;
|
|
559
|
-
}
|
|
560
|
-
if (!p.codeBlockText) {
|
|
561
|
-
p.codeBlockText = preset.codeBlock.color;
|
|
562
|
-
}
|
|
563
|
-
if (!p.blockquoteBg) {
|
|
564
|
-
p.blockquoteBg = preset.blockquote.bgColor;
|
|
565
|
-
}
|
|
566
|
-
if (!p.blockquoteBorder) {
|
|
567
|
-
p.blockquoteBorder = preset.blockquote.borderLeftColor;
|
|
568
|
-
}
|
|
569
|
-
if (!p.selectionBg) {
|
|
570
|
-
p.selectionBg = preset.selectionBg;
|
|
571
|
-
}
|
|
572
|
-
if (!p.borderColor) {
|
|
573
|
-
p.borderColor = preset.table.borderColor;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (
|
|
577
|
-
!t.bodyFontFamily ||
|
|
578
|
-
t.bodyFontFamily === "Georgia, serif"
|
|
579
|
-
) {
|
|
580
|
-
t.bodyFontFamily = preset.body.fontFamily;
|
|
581
|
-
}
|
|
582
|
-
if (!t.headingFontFamily) {
|
|
583
|
-
t.headingFontFamily = preset.headings.h1.fontFamily;
|
|
584
|
-
}
|
|
585
|
-
if (!t.baseFontSize) {
|
|
586
|
-
t.baseFontSize = `${preset.body.fontSize}px`;
|
|
587
|
-
}
|
|
588
|
-
if (t.baseLineHeight < 1.4) {
|
|
589
|
-
t.baseLineHeight = preset.body.lineHeight;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
st.fontBody = st.fontBody || preset.body.fontFamily;
|
|
593
|
-
st.fontHeading = st.fontHeading || preset.headings.h1.fontFamily;
|
|
594
|
-
st.lineHeightBody = st.lineHeightBody || preset.body.lineHeight;
|
|
595
|
-
st.lineHeightHeading =
|
|
596
|
-
st.lineHeightHeading || preset.headings.h1.lineHeight;
|
|
597
|
-
|
|
598
|
-
// 确保标题字号倍率符合范式约束
|
|
599
|
-
if (st.fontScaleH1 < 1.75) st.fontScaleH1 = preset.headings.h1.fontSize / 16;
|
|
600
|
-
if (st.fontScaleH2 < st.fontScaleH1 * 0.8)
|
|
601
|
-
st.fontScaleH2 = preset.headings.h2.fontSize / 16;
|
|
602
|
-
if (st.fontScaleH3 < st.fontScaleH2 * 0.85)
|
|
603
|
-
st.fontScaleH3 = preset.headings.h3.fontSize / 16;
|
|
604
|
-
|
|
605
|
-
const issues = validateDocumentStyle(themeToParadigm(theme));
|
|
606
|
-
logExtractorWarnings("UrlExtractor", issues.filter((i) => i.level === "error"));
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// ═══════════════════════════════════════════════════════════
|
|
610
|
-
// 工具方法:CSS 属性解析与规范化
|
|
611
|
-
// ═══════════════════════════════════════════════════════════
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* 为一组选择器查找匹配的 CSS 规则并合并结果
|
|
615
|
-
* 优先级:更具体的选择器 > 更通用的选择器 > 内联样式
|
|
616
|
-
*/
|
|
617
|
-
private resolveElementStyles(
|
|
618
|
-
rules: CssRuleMap,
|
|
619
|
-
$: cheerio.CheerioAPI,
|
|
620
|
-
selectors: string[]
|
|
621
|
-
): ElementStyles {
|
|
622
|
-
const merged: ElementStyles = {};
|
|
623
|
-
|
|
624
|
-
for (const selector of selectors) {
|
|
625
|
-
const ruleProps = this.findBestMatchingRule(rules, selector);
|
|
626
|
-
if (ruleProps) {
|
|
627
|
-
if (ruleProps.get("font-family") && !merged.fontFamily)
|
|
628
|
-
merged.fontFamily = ruleProps.get("font-family");
|
|
629
|
-
if (ruleProps.get("font-size") && !merged.fontSize)
|
|
630
|
-
merged.fontSize = ruleProps.get("font-size");
|
|
631
|
-
if (ruleProps.get("font-weight") && !merged.fontWeight)
|
|
632
|
-
merged.fontWeight = ruleProps.get("font-weight");
|
|
633
|
-
if (ruleProps.get("color") && !merged.color)
|
|
634
|
-
merged.color = ruleProps.get("color");
|
|
635
|
-
if (ruleProps.get("line-height") && !merged.lineHeight)
|
|
636
|
-
merged.lineHeight = ruleProps.get("line-height");
|
|
637
|
-
if (ruleProps.get("background-color") && !merged.backgroundColor)
|
|
638
|
-
merged.backgroundColor = ruleProps.get("background-color");
|
|
639
|
-
if (ruleProps.get("border-left-color") && !merged.borderColor)
|
|
640
|
-
merged.borderColor = ruleProps.get("border-left-color");
|
|
641
|
-
if (ruleProps.get("border-color") && !merged.borderColor)
|
|
642
|
-
merged.borderColor = ruleProps.get("border-color");
|
|
643
|
-
if (ruleProps.get("letter-spacing") && !merged.letterSpacing)
|
|
644
|
-
merged.letterSpacing = ruleProps.get("letter-spacing");
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// 补充内联样式(最高优先级)
|
|
649
|
-
for (const selector of selectors) {
|
|
650
|
-
try {
|
|
651
|
-
const el = $(selector).first();
|
|
652
|
-
if (el.length) {
|
|
653
|
-
const inlineAttr = el.attr("style");
|
|
654
|
-
if (inlineAttr) {
|
|
655
|
-
const inlineMap = this.parseInlineStyle(inlineAttr);
|
|
656
|
-
if (inlineMap.fontFamily) merged.fontFamily = inlineMap.fontFamily;
|
|
657
|
-
if (inlineMap.fontSize) merged.fontSize = inlineMap.fontSize;
|
|
658
|
-
if (inlineMap.fontWeight) merged.fontWeight = inlineMap.fontWeight;
|
|
659
|
-
if (inlineMap.color) merged.color = inlineMap.color;
|
|
660
|
-
if (inlineMap.lineHeight) merged.lineHeight = inlineMap.lineHeight;
|
|
661
|
-
if (inlineMap.backgroundColor)
|
|
662
|
-
merged.backgroundColor = inlineMap.backgroundColor;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
} catch {
|
|
666
|
-
// 选择器可能无效,跳过
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return merged;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* 在 CSS 规则中查找最佳匹配的选择器
|
|
675
|
-
*/
|
|
676
|
-
private findBestMatchingRule(
|
|
677
|
-
rules: CssRuleMap,
|
|
678
|
-
targetSelector: string
|
|
679
|
-
): Map<string, string> | undefined {
|
|
680
|
-
let bestMatch: Map<string, string> | undefined;
|
|
681
|
-
let bestSpecificity = -1;
|
|
682
|
-
|
|
683
|
-
for (const [selector, props] of rules) {
|
|
684
|
-
if (
|
|
685
|
-
selector.includes(targetSelector) ||
|
|
686
|
-
targetSelector.includes(selector.replace(/[.#\[\]>+~]/g, "").split(" ").pop() || "")
|
|
687
|
-
) {
|
|
688
|
-
const specificity = this.calculateSpecificity(selector);
|
|
689
|
-
if (specificity > bestSpecificity) {
|
|
690
|
-
bestSpecificity = specificity;
|
|
691
|
-
bestMatch = props;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
return bestMatch;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* 计算选择器特异性(简化版)
|
|
701
|
-
*/
|
|
702
|
-
private calculateSpecificity(selector: string): number {
|
|
703
|
-
let score = 0;
|
|
704
|
-
score += (selector.match(/#/g) || []).length * 100;
|
|
705
|
-
score += (selector.match(/\./g) || []).length * 10;
|
|
706
|
-
score += (selector.match(/[a-zA-Z]/g) || []).length;
|
|
707
|
-
return score;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* 解析内联 style 字符串为键值对
|
|
712
|
-
*/
|
|
713
|
-
private parseInlineStyle(styleStr: string): Partial<ElementStyles> {
|
|
714
|
-
const result: Partial<ElementStyles> = {};
|
|
715
|
-
const declarations = styleStr.split(";");
|
|
716
|
-
|
|
717
|
-
for (const decl of declarations) {
|
|
718
|
-
const colonIndex = decl.indexOf(":");
|
|
719
|
-
if (colonIndex === -1) continue;
|
|
720
|
-
|
|
721
|
-
const prop = decl.slice(0, colonIndex).trim().toLowerCase();
|
|
722
|
-
const value = decl.slice(colonIndex + 1).trim();
|
|
723
|
-
|
|
724
|
-
switch (prop) {
|
|
725
|
-
case "font-family":
|
|
726
|
-
result.fontFamily = value;
|
|
727
|
-
break;
|
|
728
|
-
case "font-size":
|
|
729
|
-
result.fontSize = value;
|
|
730
|
-
break;
|
|
731
|
-
case "font-weight":
|
|
732
|
-
result.fontWeight = value;
|
|
733
|
-
break;
|
|
734
|
-
case "color":
|
|
735
|
-
result.color = value;
|
|
736
|
-
break;
|
|
737
|
-
case "line-height":
|
|
738
|
-
result.lineHeight = value;
|
|
739
|
-
break;
|
|
740
|
-
case "background-color":
|
|
741
|
-
case "background":
|
|
742
|
-
result.backgroundColor = value;
|
|
743
|
-
break;
|
|
744
|
-
case "border-color":
|
|
745
|
-
case "border-left-color":
|
|
746
|
-
result.borderColor = value;
|
|
747
|
-
break;
|
|
748
|
-
case "letter-spacing":
|
|
749
|
-
result.letterSpacing = value;
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return result;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* 规范化字体家族名称(去除引号、清理多余空格)
|
|
759
|
-
*/
|
|
760
|
-
private normalizeFontFamily(fontFamily: string): string {
|
|
761
|
-
return fontFamily
|
|
762
|
-
.replace(/['"]/g, "")
|
|
763
|
-
.replace(/\s{2,}/g, " ")
|
|
764
|
-
.trim();
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* 规范化字号为 px 单位
|
|
769
|
-
*/
|
|
770
|
-
private normalizeFontSize(sizeStr: string): string {
|
|
771
|
-
return `${this.normalizeFontSizeToPx(sizeStr, 16)}px`;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* 将字号转换为 px 数值
|
|
776
|
-
*/
|
|
777
|
-
private normalizeFontSizeToPx(sizeStr: string, rootPx: number): number {
|
|
778
|
-
const trimmed = sizeStr.trim();
|
|
779
|
-
|
|
780
|
-
// em/rem 单位转换
|
|
781
|
-
const emMatch = trimmed.match(/^([\d.]+)(em|rem)$/i);
|
|
782
|
-
if (emMatch) {
|
|
783
|
-
return parseFloat(emMatch[1]) * rootPx;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// pt → px 转换 (1pt ≈ 1.333px)
|
|
787
|
-
const ptMatch = trimmed.match(/^([\d.]+)pt$/i);
|
|
788
|
-
if (ptMatch) {
|
|
789
|
-
return parseFloat(ptMatch[1]) * 1.333;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// 直接是 px 或无单位数字
|
|
793
|
-
const pxMatch = trimmed.match(/^([\d.]+)(px)?$/i);
|
|
794
|
-
if (pxMatch) {
|
|
795
|
-
return parseFloat(pxMatch[1]);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// 关键字号映射
|
|
799
|
-
const keywordSizes: Record<string, number> = {
|
|
800
|
-
"xx-small": 10,
|
|
801
|
-
"x-small": 12,
|
|
802
|
-
small: 14,
|
|
803
|
-
medium: 16,
|
|
804
|
-
large: 18,
|
|
805
|
-
"x-large": 20,
|
|
806
|
-
"xx-large": 24,
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
if (keywordSizes[trimmed]) {
|
|
810
|
-
return keywordSizes[trimmed];
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return rootPx;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* 规范化颜色值为标准 hex 格式
|
|
818
|
-
*/
|
|
819
|
-
private normalizeColor(colorStr: string): string {
|
|
820
|
-
const trimmed = colorStr.trim().toLowerCase();
|
|
821
|
-
|
|
822
|
-
// 已经是 hex 格式
|
|
823
|
-
if (/^#[0-9a-f]{3,8}$/.test(trimmed)) {
|
|
824
|
-
return trimmed;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// rgb()/rgba() 格式
|
|
828
|
-
const rgbMatch = trimmed.match(
|
|
829
|
-
/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/
|
|
830
|
-
);
|
|
831
|
-
if (rgbMatch) {
|
|
832
|
-
const r = parseInt(rgbMatch[1]);
|
|
833
|
-
const g = parseInt(rgbMatch[2]);
|
|
834
|
-
const b = parseInt(rgbMatch[3]);
|
|
835
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// 透明色返回空字符串
|
|
839
|
-
if (trimmed === "transparent" || trimmed === "none" || trimmed === "initial" || trimmed === "inherit") {
|
|
840
|
-
return "";
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// 返回原值(可能是 named color 等)
|
|
844
|
-
return trimmed;
|
|
845
|
-
}
|
|
846
|
-
}
|