@bytechain.cn/colamd-themes 0.3.0

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