@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.
Files changed (99) hide show
  1. package/.claude/skills/colamd-themes-preview.skill.md +333 -0
  2. package/.claude/skills/colamd-themes.skill.md +10 -0
  3. package/.dsh/skills/colamd-themes/SKILL.md +14 -0
  4. package/.github/supported-topics.yml +17 -0
  5. package/.hermes/skills/tools/colamd-themes/SKILL.md +14 -0
  6. package/.openclaw/skills/colamd-themes/SKILL.md +1 -37
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -36
  9. package/CHANGELOG.md +331 -241
  10. package/DSH_SUPPORT_SUMMARY.md +87 -0
  11. package/README.md +414 -384
  12. package/README_CN.md +412 -384
  13. package/RELEASENOTE.md +541 -389
  14. package/dist/cli.js +36 -55
  15. package/dist/cli.js.map +1 -1
  16. package/dist/color-utils.d.ts +1 -0
  17. package/dist/color-utils.js +25 -6
  18. package/dist/color-utils.js.map +1 -1
  19. package/dist/extractors/pdf-extractor.js +6 -1
  20. package/dist/extractors/pdf-extractor.js.map +1 -1
  21. package/dist/extractors/url-extractor.d.ts +0 -4
  22. package/dist/extractors/url-extractor.js +13 -126
  23. package/dist/extractors/url-extractor.js.map +1 -1
  24. package/dist/generator.d.ts +1 -1
  25. package/dist/generator.js +32 -11
  26. package/dist/generator.js.map +1 -1
  27. package/dist/models.d.ts +5 -4
  28. package/dist/models.js +2 -3
  29. package/dist/models.js.map +1 -1
  30. package/dist/renderer.d.ts +6 -3
  31. package/dist/renderer.js +54 -93
  32. package/dist/renderer.js.map +1 -1
  33. package/dist/templates/template.css +220 -498
  34. package/dist/templates/theme-paradigm.md +530 -658
  35. package/dist/templates/theme-template.hbs +256 -370
  36. package/dist/theme-store.js +19 -3
  37. package/dist/theme-store.js.map +1 -1
  38. package/dist/utils/document-paradigm.d.ts +12 -0
  39. package/dist/utils/document-paradigm.js +109 -0
  40. package/dist/utils/document-paradigm.js.map +1 -0
  41. package/dist/validators.d.ts +4 -1
  42. package/dist/validators.js +115 -1
  43. package/dist/validators.js.map +1 -1
  44. package/mcp-server/dist/index.d.ts +13 -0
  45. package/mcp-server/dist/index.d.ts.map +1 -0
  46. package/mcp-server/dist/index.js +0 -0
  47. package/mcp-server/dist/index.js.map +1 -0
  48. package/mcp-server/package-lock.json +1759 -0
  49. package/mcp-server/package.json +24 -0
  50. package/mcp-server/src/index.ts +0 -0
  51. package/mcp-server/tsconfig.json +19 -0
  52. package/package.json +54 -36
  53. package/skills/build-skills.sh +30 -5
  54. package/skills/colamd-themes/SKILL.md +180 -178
  55. package/src/cli.ts +474 -487
  56. package/src/color-systems.ts +289 -289
  57. package/src/color-utils.ts +137 -120
  58. package/src/contrast.ts +66 -66
  59. package/src/docx-style-paradigm.ts +811 -811
  60. package/src/export-cli.ts +356 -356
  61. package/src/extractors/base.ts +8 -8
  62. package/src/extractors/docx-extractor.ts +417 -417
  63. package/src/extractors/pdf-extractor.ts +533 -527
  64. package/src/extractors/url-extractor.ts +846 -981
  65. package/src/generator.ts +197 -169
  66. package/src/index.ts +29 -29
  67. package/src/models.ts +212 -212
  68. package/src/renderer.ts +310 -359
  69. package/src/templates/template.css +220 -498
  70. package/src/templates/theme-paradigm.md +530 -658
  71. package/src/templates/theme-template.hbs +256 -370
  72. package/src/theme-store.ts +185 -170
  73. package/src/types/pdf-parse.d.ts +12 -12
  74. package/src/utils/cli-utils.ts +84 -84
  75. package/src/utils/document-paradigm.ts +118 -0
  76. package/src/validators.ts +298 -176
  77. package/themes/academic-paper-pt.css +353 -1805
  78. package/themes/academic-paper.css +350 -1099
  79. package/themes/academic-style.css +342 -2430
  80. package/themes/colly.css +256 -370
  81. package/themes/elegant.css +288 -671
  82. package/themes/forest-ink.css +240 -664
  83. package/themes/guizang.css +250 -732
  84. package/themes/hermes-security.css +279 -0
  85. package/themes/meeting-minutes.css +256 -370
  86. package/themes/pixso-design.css +278 -1261
  87. package/themes/salvator-blog.css +256 -370
  88. package/themes/shimo-theme.css +256 -0
  89. package/themes/swiss-design.css +285 -746
  90. package/themes/template.css +220 -498
  91. package/themes/trae-blue.css +280 -737
  92. package/themes/word-contract.css +249 -0
  93. package/themes-preview/demo-content.md +187 -187
  94. package/tsconfig.json +18 -18
  95. package/src/mermaid-presets.ts +0 -142
  96. package/themes/swiss-design copy.css +0 -746
  97. package/themes-preview/colly-preview.html +0 -623
  98. package/themes-preview/meeting-minutes-preview.html +0 -625
  99. package/themes-preview/salvator-blog-preview.html +0 -623
@@ -1,417 +1,417 @@
1
- /**
2
- * DOCX 文档样式提取器 — v2.0
3
- *
4
- * 增强功能:
5
- * 1. 提取各级标题(H1-H6)的完整字体样式定义
6
- * 2. 提取正文字体、字号、颜色、行间距
7
- * 3. 提取表格样式(表头/单元格字体、背景色、边框)
8
- * 4. 对无法提取的样式应用 DocxStyleParadigm 标准参考值
9
- * 5. 自动验证 WCAG 对比度合规性
10
- */
11
-
12
- import { readFileSync } from "node:fs";
13
- import { basename } from "node:path";
14
- import mammoth from "mammoth";
15
- import type { Extractor } from "./base.js";
16
- import {
17
- makeThemeStyle,
18
- type ThemeStyle,
19
- type ColorPalette,
20
- type Typography,
21
- } from "../models.js";
22
- import { normalizeThemeStyle, hexToRgb, rgbToHex, lighten, darken } from "../color-utils.js";
23
- import {
24
- ACADEMIC_PAPER_DEFAULTS,
25
- getRecommendedPreset,
26
- validateDocumentStyle,
27
- normalizeForPrinting,
28
- type DocumentStyleParadigm,
29
- type FontStyleDefinition,
30
- type TableCellStyle,
31
- } from "../docx-style-paradigm.js";
32
-
33
- export class DocxExtractor implements Extractor {
34
- canHandle(source: string): boolean {
35
- return /\.docx$/i.test(source);
36
- }
37
-
38
- async extract(source: string): Promise<ThemeStyle> {
39
- const buffer = readFileSync(source);
40
- const name = basename(source).replace(/\.docx$/i, "");
41
- const theme = makeThemeStyle(name, "docx", source);
42
-
43
- const result = await mammoth.convertToHtml(
44
- { buffer },
45
- {
46
- styleMap: [
47
- "p[style-name='Heading 1'] => h1:fresh",
48
- "p[style-name='Heading 2'] => h2:fresh",
49
- "p[style-name='Heading 3'] => h3:fresh",
50
- "p[style-name='Heading 4'] => h4:fresh",
51
- "p[style-name='Heading 5'] => h5:fresh",
52
- "p[style-name='Heading 6'] => h6:fresh",
53
- "r[style-name='Strong'] => strong",
54
- ],
55
- }
56
- );
57
-
58
- if (result.value) {
59
- this.extractDetailedStyles(theme, result.value);
60
- }
61
-
62
- this.applyParadigmDefaults(theme);
63
- normalizeThemeStyle(theme);
64
- return theme;
65
- }
66
-
67
- /**
68
- * 从 HTML 输出中提取详细的文档元素样式
69
- */
70
- private extractDetailedStyles(theme: ThemeStyle, html: string): void {
71
- const p = theme.palette;
72
- const t = theme.typography;
73
-
74
- this.extractBodyStyles(t, html);
75
- this.extractHeadingStyles(theme, html);
76
- this.extractTableStyles(p, html);
77
- this.extractColorPalette(p, html);
78
- this.extractBlockStyles(p, html);
79
- }
80
-
81
- /**
82
- * 提取正文基础样式(字体、字号、行高)
83
- */
84
- private extractBodyStyles(typography: Typography, html: string): void {
85
- const fontMatch = html.match(/font-family:\s*['"]?([^;'"]+)['"]?/i);
86
- if (fontMatch) {
87
- typography.bodyFontFamily = fontMatch[1].trim();
88
- }
89
-
90
- const fontSizeMatch = html.match(/font-size:\s*([\d.]+)(pt|px)/i);
91
- if (fontSizeMatch) {
92
- const size = parseFloat(fontSizeMatch[1]);
93
- const unit = fontSizeMatch[2];
94
- typography.baseFontSize = unit === "pt" ? `${size * 1.333}px` : `${size}px`;
95
- }
96
- }
97
-
98
- /**
99
- * 提取各级标题的详细样式
100
- */
101
- private extractHeadingStyles(theme: ThemeStyle, html: string): void {
102
- const headingLevels = ["h1", "h2", "h3", "h4", "h5", "h6"];
103
-
104
- for (const level of headingLevels) {
105
- const regex = new RegExp(
106
- `<${level}[^>]*style="[^"]*"`,
107
- "gi"
108
- );
109
- let match = regex.exec(html);
110
-
111
- if (match) {
112
- const styleStr = match[0];
113
- const extracted = this.parseInlineStyle(styleStr);
114
-
115
- if (extracted.fontSize) {
116
- const size = parseFloat(extracted.fontSize);
117
- const unit = extracted.fontSize.replace(/[\d.]/g, "") || "px";
118
- const pxSize = unit === "pt" ? size * 1.333 : size;
119
-
120
- switch (level) {
121
- case "h1": theme.seedTypography.fontScaleH1 = pxSize / 16; break;
122
- case "h2": theme.seedTypography.fontScaleH2 = pxSize / 16; break;
123
- case "h3": theme.seedTypography.fontScaleH3 = pxSize / 16; break;
124
- case "h4": theme.seedTypography.fontScaleH4 = pxSize / 16; break;
125
- case "h5": theme.seedTypography.fontScaleH5 = pxSize / 16; break;
126
- case "h6": theme.seedTypography.fontScaleH6 = pxSize / 16; break;
127
- }
128
- }
129
-
130
- if (extracted.fontWeight) {
131
- const weight = parseInt(extracted.fontWeight) || 700;
132
- if (weight > theme.typography.headingWeight) {
133
- theme.typography.headingWeight = weight;
134
- }
135
- }
136
-
137
- if (extracted.color && level === "h1") {
138
- theme.palette.headingColor = extracted.color;
139
- }
140
- }
141
- }
142
- }
143
-
144
- /**
145
- * 提取表格详细样式(表头、单元格、边框)
146
- */
147
- private extractTableStyles(palette: ColorPalette, html: string): void {
148
- const thRegex = /<th[^>]*style="([^"]*)"/gi;
149
- let thMatch = thRegex.exec(html);
150
-
151
- if (thMatch) {
152
- const thStyle = this.parseInlineStyle(thMatch[0]);
153
- if (thStyle.backgroundColor) {
154
- palette.tableHeaderBg = thStyle.backgroundColor;
155
- }
156
- if (thStyle.color) {
157
- palette.headingColor = palette.headingColor || thStyle.color;
158
- }
159
- if (thStyle.fontSize) {
160
- const size = parseFloat(thStyle.fontSize);
161
- palette.tableHeaderBg = palette.tableHeaderBg || "#f0f0f0";
162
- }
163
- }
164
-
165
- const tdRegex = /<td[^>]*style="([^"]*)"/gi;
166
- let tdMatch = tdRegex.exec(html);
167
- if (tdMatch) {
168
- const tdStyle = this.parseInlineStyle(tdMatch[0]);
169
- if (tdStyle.backgroundColor) {
170
- palette.codeBg = tdStyle.backgroundColor;
171
- }
172
- }
173
-
174
- const tableBorderMatch = html.match(/<table[^>]*style="[^"]*border[^;]*:[^;]*([^;"'\s]+)/i);
175
- if (tableBorderMatch) {
176
- palette.borderColor = palette.borderColor || "#cccccc";
177
- }
178
-
179
- if (!palette.tableHeaderBg) {
180
- palette.tableHeaderBg = "#f0f0f0";
181
- }
182
- }
183
-
184
- /**
185
- * 提取全局色彩方案
186
- */
187
- private extractColorPalette(palette: ColorPalette, html: string): void {
188
- const colorMatch = html.match(/color:\s*(#[0-9a-fA-F]{3,6})/i);
189
- if (colorMatch) {
190
- palette.textColor = colorMatch[1];
191
- }
192
-
193
- const bgMatch = html.match(/background(?:-color)?:\s*(#[0-9a-fA-F]{3,6})/i);
194
- if (bgMatch && bgMatch[1].toLowerCase() !== "#ffffff") {
195
- palette.bgColor = bgMatch[1];
196
- }
197
-
198
- const strongMatch = html.match(/<strong[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
199
- if (strongMatch) {
200
- palette.accentColor = strongMatch[1];
201
- }
202
-
203
- const linkMatch = html.match(/<a[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
204
- if (linkMatch) {
205
- palette.accentColor = linkMatch[1];
206
- }
207
- }
208
-
209
- /**
210
- * 提取引用块和代码块样式
211
- */
212
- private extractBlockStyles(palette: ColorPalette, html: string): void {
213
- const preMatch = html.match(/<pre[^>]*style="[^"]*background(?:-color)?:\s*(#[0-9a-fA-F]{3,6})/i);
214
- if (preMatch) {
215
- palette.codeBlockBg = preMatch[1];
216
- }
217
-
218
- const preColorMatch = html.match(/<pre[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
219
- if (preColorMatch) {
220
- palette.codeBlockText = preColorMatch[1];
221
- }
222
-
223
- if (!palette.blockquoteBorder) {
224
- palette.blockquoteBorder = palette.accentColor;
225
- }
226
- }
227
-
228
- /**
229
- * 解析内联 style 字符串为键值对对象
230
- */
231
- private parseInlineStyle(styleTag: string): Record<string, string> {
232
- const styleMatch = styleTag.match(/style="([^"]*)"/i);
233
- if (!styleMatch) return {};
234
-
235
- const styles: Record<string, string> = {};
236
- const declarations = styleMatch[1].split(";");
237
-
238
- for (const decl of declarations) {
239
- const [prop, ...valueParts] = decl.split(":").map((s) => s.trim());
240
- if (prop && valueParts.length > 0) {
241
- const camelProp = prop.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
242
- styles[camelProp] = valueParts.join(":").trim();
243
- }
244
- }
245
-
246
- return styles;
247
- }
248
-
249
- /**
250
- * 应用 DocxStyleParadigm 标准参考值填充缺失字段
251
- */
252
- private applyParadigmDefaults(theme: ThemeStyle): void {
253
- const preset = getRecommendedPreset();
254
- const p = theme.palette;
255
- const t = theme.typography;
256
- const st = theme.seedTypography;
257
-
258
- if (!p.textColor || p.textColor === "#000000") {
259
- p.textColor = preset.body.color;
260
- }
261
- if (!p.headingColor) {
262
- p.headingColor = preset.headings.h1.color;
263
- }
264
- if (!p.bgColor || p.bgColor === "#ffffff") {
265
- p.bgColor = preset.pageBackground;
266
- }
267
- if (!p.textMuted) {
268
- p.textMuted = preset.mutedTextColor;
269
- }
270
- if (!p.accentColor || p.accentColor === "#0000ff") {
271
- p.accentColor = preset.accentColor;
272
- }
273
- if (!p.tableHeaderBg) {
274
- p.tableHeaderBg = preset.table.headerBgColor;
275
- }
276
- if (!p.codeBg) {
277
- p.codeBg = preset.inlineCode.bgColor;
278
- }
279
- if (!p.codeBlockBg) {
280
- p.codeBlockBg = preset.codeBlock.bgColor;
281
- }
282
- if (!p.blockquoteBg) {
283
- p.blockquoteBg = preset.blockquote.bgColor;
284
- }
285
- if (!p.selectionBg) {
286
- p.selectionBg = preset.selectionBg;
287
- }
288
-
289
- if (!t.bodyFontFamily || t.bodyFontFamily === "Georgia, serif") {
290
- t.bodyFontFamily = preset.body.fontFamily;
291
- }
292
- if (!t.headingFontFamily) {
293
- t.headingFontFamily = preset.headings.h1.fontFamily;
294
- }
295
- if (!t.baseFontSize) {
296
- t.baseFontSize = `${preset.body.fontSize}px`;
297
- }
298
- if (t.baseLineHeight < 1.5) {
299
- t.baseLineHeight = preset.body.lineHeight;
300
- }
301
-
302
- st.fontBody = preset.body.fontFamily;
303
- st.fontHeading = preset.headings.h1.fontFamily;
304
- st.lineHeightBody = preset.body.lineHeight;
305
- st.lineHeightHeading = preset.headings.h1.lineHeight;
306
-
307
- const issues = validateDocumentStyle(this.themeToParadigm(theme));
308
- for (const issue of issues) {
309
- if (issue.level === "error") {
310
- console.warn(`[DocxExtractor] ${issue.element}: ${issue.message}`);
311
- }
312
- }
313
- }
314
-
315
- /**
316
- * 将 ThemeStyle 转换为 DocumentStyleParadigm 用于验证
317
- */
318
- private themeToParadigm(theme: ThemeStyle): DocumentStyleParadigm {
319
- return {
320
- pageBackground: theme.palette.bgColor,
321
- headings: {
322
- h1: {
323
- fontFamily: theme.seedTypography.fontHeading,
324
- fontSize: 16 * theme.seedTypography.fontScaleH1,
325
- fontWeight: theme.typography.headingWeight,
326
- color: theme.palette.headingColor,
327
- lineHeight: theme.seedTypography.lineHeightHeading,
328
- },
329
- h2: {
330
- fontFamily: theme.seedTypography.fontHeading,
331
- fontSize: 16 * theme.seedTypography.fontScaleH2,
332
- fontWeight: theme.typography.headingWeight - 100 || 600,
333
- color: theme.palette.headingColor,
334
- lineHeight: theme.seedTypography.lineHeightHeading,
335
- },
336
- h3: {
337
- fontFamily: theme.seedTypography.fontHeading,
338
- fontSize: 16 * theme.seedTypography.fontScaleH3,
339
- fontWeight: 600,
340
- color: theme.palette.headingColor,
341
- lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
342
- },
343
- h4: {
344
- fontFamily: theme.seedTypography.fontHeading,
345
- fontSize: 16 * theme.seedTypography.fontScaleH4,
346
- fontWeight: 600,
347
- color: theme.palette.textMuted,
348
- lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
349
- },
350
- h5: {
351
- fontFamily: theme.seedTypography.fontHeading,
352
- fontSize: 16 * theme.seedTypography.fontScaleH5,
353
- fontWeight: 500,
354
- color: theme.palette.textMuted,
355
- lineHeight: theme.seedTypography.lineHeightBody - 0.25,
356
- },
357
- h6: {
358
- fontFamily: theme.seedTypography.fontHeading,
359
- fontSize: 16 * theme.seedTypography.fontScaleH6,
360
- fontWeight: 500,
361
- color: theme.palette.textMuted,
362
- lineHeight: theme.seedTypography.lineHeightBody - 0.2,
363
- },
364
- },
365
- body: {
366
- fontFamily: theme.seedTypography.fontBody,
367
- fontSize: parseInt(theme.typography.baseFontSize) || 16,
368
- fontWeight: 400,
369
- color: theme.palette.textColor,
370
- lineHeight: theme.typography.baseLineHeight,
371
- },
372
- blockquote: {
373
- fontFamily: theme.seedTypography.fontBody,
374
- fontSize: Math.max(13, (parseInt(theme.typography.baseFontSize) || 16) - 2),
375
- fontWeight: 400,
376
- color: theme.palette.textMuted,
377
- lineHeight: theme.typography.baseLineHeight - 0.15,
378
- borderLeftColor: theme.palette.blockquoteBorder,
379
- bgColor: theme.palette.blockquoteBg,
380
- },
381
- codeBlock: {
382
- fontFamily: theme.typography.codeFontFamily,
383
- fontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
384
- fontWeight: 400,
385
- color: theme.palette.codeBlockText || theme.palette.textColor,
386
- lineHeight: theme.seedTypography.lineHeightCode,
387
- bgColor: theme.palette.codeBlockBg,
388
- },
389
- inlineCode: {
390
- fontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
391
- color: theme.palette.accentColor,
392
- bgColor: theme.palette.codeBg,
393
- },
394
- table: {
395
- headerFontFamily: theme.seedTypography.fontHeading,
396
- headerFontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
397
- headerFontWeight: 700,
398
- headerColor: "#ffffff",
399
- headerBgColor: theme.palette.tableHeaderBg,
400
- headerLineHeight: 1.4,
401
-
402
- cellFontFamily: theme.seedTypography.fontBody,
403
- cellFontSize: Math.max(11, (parseInt(theme.typography.baseFontSize) || 16) - 4),
404
- cellColor: theme.palette.textColor,
405
- cellBgColor: theme.palette.bgColor,
406
- cellLineHeight: 1.5,
407
- cellPadding: "8px 12px",
408
- borderColor: theme.palette.borderColor,
409
- borderWidth: "1px",
410
- },
411
-
412
- accentColor: theme.palette.accentColor,
413
- mutedTextColor: theme.palette.textMuted,
414
- selectionBg: theme.palette.selectionBg,
415
- };
416
- }
417
- }
1
+ /**
2
+ * DOCX 文档样式提取器 — v2.0
3
+ *
4
+ * 增强功能:
5
+ * 1. 提取各级标题(H1-H6)的完整字体样式定义
6
+ * 2. 提取正文字体、字号、颜色、行间距
7
+ * 3. 提取表格样式(表头/单元格字体、背景色、边框)
8
+ * 4. 对无法提取的样式应用 DocxStyleParadigm 标准参考值
9
+ * 5. 自动验证 WCAG 对比度合规性
10
+ */
11
+
12
+ import { readFileSync } from "node:fs";
13
+ import { basename } from "node:path";
14
+ import mammoth from "mammoth";
15
+ import type { Extractor } from "./base.js";
16
+ import {
17
+ makeThemeStyle,
18
+ type ThemeStyle,
19
+ type ColorPalette,
20
+ type Typography,
21
+ } from "../models.js";
22
+ import { normalizeThemeStyle, hexToRgb, rgbToHex, lighten, darken } from "../color-utils.js";
23
+ import {
24
+ ACADEMIC_PAPER_DEFAULTS,
25
+ getRecommendedPreset,
26
+ validateDocumentStyle,
27
+ normalizeForPrinting,
28
+ type DocumentStyleParadigm,
29
+ type FontStyleDefinition,
30
+ type TableCellStyle,
31
+ } from "../docx-style-paradigm.js";
32
+
33
+ export class DocxExtractor implements Extractor {
34
+ canHandle(source: string): boolean {
35
+ return /\.docx$/i.test(source);
36
+ }
37
+
38
+ async extract(source: string): Promise<ThemeStyle> {
39
+ const buffer = readFileSync(source);
40
+ const name = basename(source).replace(/\.docx$/i, "");
41
+ const theme = makeThemeStyle(name, "docx", source);
42
+
43
+ const result = await mammoth.convertToHtml(
44
+ { buffer },
45
+ {
46
+ styleMap: [
47
+ "p[style-name='Heading 1'] => h1:fresh",
48
+ "p[style-name='Heading 2'] => h2:fresh",
49
+ "p[style-name='Heading 3'] => h3:fresh",
50
+ "p[style-name='Heading 4'] => h4:fresh",
51
+ "p[style-name='Heading 5'] => h5:fresh",
52
+ "p[style-name='Heading 6'] => h6:fresh",
53
+ "r[style-name='Strong'] => strong",
54
+ ],
55
+ }
56
+ );
57
+
58
+ if (result.value) {
59
+ this.extractDetailedStyles(theme, result.value);
60
+ }
61
+
62
+ this.applyParadigmDefaults(theme);
63
+ normalizeThemeStyle(theme);
64
+ return theme;
65
+ }
66
+
67
+ /**
68
+ * 从 HTML 输出中提取详细的文档元素样式
69
+ */
70
+ private extractDetailedStyles(theme: ThemeStyle, html: string): void {
71
+ const p = theme.palette;
72
+ const t = theme.typography;
73
+
74
+ this.extractBodyStyles(t, html);
75
+ this.extractHeadingStyles(theme, html);
76
+ this.extractTableStyles(p, html);
77
+ this.extractColorPalette(p, html);
78
+ this.extractBlockStyles(p, html);
79
+ }
80
+
81
+ /**
82
+ * 提取正文基础样式(字体、字号、行高)
83
+ */
84
+ private extractBodyStyles(typography: Typography, html: string): void {
85
+ const fontMatch = html.match(/font-family:\s*['"]?([^;'"]+)['"]?/i);
86
+ if (fontMatch) {
87
+ typography.bodyFontFamily = fontMatch[1].trim();
88
+ }
89
+
90
+ const fontSizeMatch = html.match(/font-size:\s*([\d.]+)(pt|px)/i);
91
+ if (fontSizeMatch) {
92
+ const size = parseFloat(fontSizeMatch[1]);
93
+ const unit = fontSizeMatch[2];
94
+ typography.baseFontSize = unit === "pt" ? `${size * 1.333}px` : `${size}px`;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 提取各级标题的详细样式
100
+ */
101
+ private extractHeadingStyles(theme: ThemeStyle, html: string): void {
102
+ const headingLevels = ["h1", "h2", "h3", "h4", "h5", "h6"];
103
+
104
+ for (const level of headingLevels) {
105
+ const regex = new RegExp(
106
+ `<${level}[^>]*style="[^"]*"`,
107
+ "gi"
108
+ );
109
+ let match = regex.exec(html);
110
+
111
+ if (match) {
112
+ const styleStr = match[0];
113
+ const extracted = this.parseInlineStyle(styleStr);
114
+
115
+ if (extracted.fontSize) {
116
+ const size = parseFloat(extracted.fontSize);
117
+ const unit = extracted.fontSize.replace(/[\d.]/g, "") || "px";
118
+ const pxSize = unit === "pt" ? size * 1.333 : size;
119
+
120
+ switch (level) {
121
+ case "h1": theme.seedTypography.fontScaleH1 = pxSize / 16; break;
122
+ case "h2": theme.seedTypography.fontScaleH2 = pxSize / 16; break;
123
+ case "h3": theme.seedTypography.fontScaleH3 = pxSize / 16; break;
124
+ case "h4": theme.seedTypography.fontScaleH4 = pxSize / 16; break;
125
+ case "h5": theme.seedTypography.fontScaleH5 = pxSize / 16; break;
126
+ case "h6": theme.seedTypography.fontScaleH6 = pxSize / 16; break;
127
+ }
128
+ }
129
+
130
+ if (extracted.fontWeight) {
131
+ const weight = parseInt(extracted.fontWeight) || 700;
132
+ if (weight > theme.typography.headingWeight) {
133
+ theme.typography.headingWeight = weight;
134
+ }
135
+ }
136
+
137
+ if (extracted.color && level === "h1") {
138
+ theme.palette.headingColor = extracted.color;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 提取表格详细样式(表头、单元格、边框)
146
+ */
147
+ private extractTableStyles(palette: ColorPalette, html: string): void {
148
+ const thRegex = /<th[^>]*style="([^"]*)"/gi;
149
+ let thMatch = thRegex.exec(html);
150
+
151
+ if (thMatch) {
152
+ const thStyle = this.parseInlineStyle(thMatch[0]);
153
+ if (thStyle.backgroundColor) {
154
+ palette.tableHeaderBg = thStyle.backgroundColor;
155
+ }
156
+ if (thStyle.color) {
157
+ palette.headingColor = palette.headingColor || thStyle.color;
158
+ }
159
+ if (thStyle.fontSize) {
160
+ const size = parseFloat(thStyle.fontSize);
161
+ palette.tableHeaderBg = palette.tableHeaderBg || "#f0f0f0";
162
+ }
163
+ }
164
+
165
+ const tdRegex = /<td[^>]*style="([^"]*)"/gi;
166
+ let tdMatch = tdRegex.exec(html);
167
+ if (tdMatch) {
168
+ const tdStyle = this.parseInlineStyle(tdMatch[0]);
169
+ if (tdStyle.backgroundColor) {
170
+ palette.codeBg = tdStyle.backgroundColor;
171
+ }
172
+ }
173
+
174
+ const tableBorderMatch = html.match(/<table[^>]*style="[^"]*border[^;]*:[^;]*([^;"'\s]+)/i);
175
+ if (tableBorderMatch) {
176
+ palette.borderColor = palette.borderColor || "#cccccc";
177
+ }
178
+
179
+ if (!palette.tableHeaderBg) {
180
+ palette.tableHeaderBg = "#f0f0f0";
181
+ }
182
+ }
183
+
184
+ /**
185
+ * 提取全局色彩方案
186
+ */
187
+ private extractColorPalette(palette: ColorPalette, html: string): void {
188
+ const colorMatch = html.match(/color:\s*(#[0-9a-fA-F]{3,6})/i);
189
+ if (colorMatch) {
190
+ palette.textColor = colorMatch[1];
191
+ }
192
+
193
+ const bgMatch = html.match(/background(?:-color)?:\s*(#[0-9a-fA-F]{3,6})/i);
194
+ if (bgMatch && bgMatch[1].toLowerCase() !== "#ffffff") {
195
+ palette.bgColor = bgMatch[1];
196
+ }
197
+
198
+ const strongMatch = html.match(/<strong[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
199
+ if (strongMatch) {
200
+ palette.accentColor = strongMatch[1];
201
+ }
202
+
203
+ const linkMatch = html.match(/<a[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
204
+ if (linkMatch) {
205
+ palette.accentColor = linkMatch[1];
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 提取引用块和代码块样式
211
+ */
212
+ private extractBlockStyles(palette: ColorPalette, html: string): void {
213
+ const preMatch = html.match(/<pre[^>]*style="[^"]*background(?:-color)?:\s*(#[0-9a-fA-F]{3,6})/i);
214
+ if (preMatch) {
215
+ palette.codeBlockBg = preMatch[1];
216
+ }
217
+
218
+ const preColorMatch = html.match(/<pre[^>]*style="[^"]*color:\s*(#[0-9a-fA-F]{3,6})/i);
219
+ if (preColorMatch) {
220
+ palette.codeBlockText = preColorMatch[1];
221
+ }
222
+
223
+ if (!palette.blockquoteBorder) {
224
+ palette.blockquoteBorder = palette.accentColor;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * 解析内联 style 字符串为键值对对象
230
+ */
231
+ private parseInlineStyle(styleTag: string): Record<string, string> {
232
+ const styleMatch = styleTag.match(/style="([^"]*)"/i);
233
+ if (!styleMatch) return {};
234
+
235
+ const styles: Record<string, string> = {};
236
+ const declarations = styleMatch[1].split(";");
237
+
238
+ for (const decl of declarations) {
239
+ const [prop, ...valueParts] = decl.split(":").map((s) => s.trim());
240
+ if (prop && valueParts.length > 0) {
241
+ const camelProp = prop.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
242
+ styles[camelProp] = valueParts.join(":").trim();
243
+ }
244
+ }
245
+
246
+ return styles;
247
+ }
248
+
249
+ /**
250
+ * 应用 DocxStyleParadigm 标准参考值填充缺失字段
251
+ */
252
+ private applyParadigmDefaults(theme: ThemeStyle): void {
253
+ const preset = getRecommendedPreset();
254
+ const p = theme.palette;
255
+ const t = theme.typography;
256
+ const st = theme.seedTypography;
257
+
258
+ if (!p.textColor || p.textColor === "#000000") {
259
+ p.textColor = preset.body.color;
260
+ }
261
+ if (!p.headingColor) {
262
+ p.headingColor = preset.headings.h1.color;
263
+ }
264
+ if (!p.bgColor || p.bgColor === "#ffffff") {
265
+ p.bgColor = preset.pageBackground;
266
+ }
267
+ if (!p.textMuted) {
268
+ p.textMuted = preset.mutedTextColor;
269
+ }
270
+ if (!p.accentColor || p.accentColor === "#0000ff") {
271
+ p.accentColor = preset.accentColor;
272
+ }
273
+ if (!p.tableHeaderBg) {
274
+ p.tableHeaderBg = preset.table.headerBgColor;
275
+ }
276
+ if (!p.codeBg) {
277
+ p.codeBg = preset.inlineCode.bgColor;
278
+ }
279
+ if (!p.codeBlockBg) {
280
+ p.codeBlockBg = preset.codeBlock.bgColor;
281
+ }
282
+ if (!p.blockquoteBg) {
283
+ p.blockquoteBg = preset.blockquote.bgColor;
284
+ }
285
+ if (!p.selectionBg) {
286
+ p.selectionBg = preset.selectionBg;
287
+ }
288
+
289
+ if (!t.bodyFontFamily || t.bodyFontFamily === "Georgia, serif") {
290
+ t.bodyFontFamily = preset.body.fontFamily;
291
+ }
292
+ if (!t.headingFontFamily) {
293
+ t.headingFontFamily = preset.headings.h1.fontFamily;
294
+ }
295
+ if (!t.baseFontSize) {
296
+ t.baseFontSize = `${preset.body.fontSize}px`;
297
+ }
298
+ if (t.baseLineHeight < 1.5) {
299
+ t.baseLineHeight = preset.body.lineHeight;
300
+ }
301
+
302
+ st.fontBody = preset.body.fontFamily;
303
+ st.fontHeading = preset.headings.h1.fontFamily;
304
+ st.lineHeightBody = preset.body.lineHeight;
305
+ st.lineHeightHeading = preset.headings.h1.lineHeight;
306
+
307
+ const issues = validateDocumentStyle(this.themeToParadigm(theme));
308
+ for (const issue of issues) {
309
+ if (issue.level === "error") {
310
+ console.warn(`[DocxExtractor] ${issue.element}: ${issue.message}`);
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 将 ThemeStyle 转换为 DocumentStyleParadigm 用于验证
317
+ */
318
+ private themeToParadigm(theme: ThemeStyle): DocumentStyleParadigm {
319
+ return {
320
+ pageBackground: theme.palette.bgColor,
321
+ headings: {
322
+ h1: {
323
+ fontFamily: theme.seedTypography.fontHeading,
324
+ fontSize: 16 * theme.seedTypography.fontScaleH1,
325
+ fontWeight: theme.typography.headingWeight,
326
+ color: theme.palette.headingColor,
327
+ lineHeight: theme.seedTypography.lineHeightHeading,
328
+ },
329
+ h2: {
330
+ fontFamily: theme.seedTypography.fontHeading,
331
+ fontSize: 16 * theme.seedTypography.fontScaleH2,
332
+ fontWeight: theme.typography.headingWeight - 100 || 600,
333
+ color: theme.palette.headingColor,
334
+ lineHeight: theme.seedTypography.lineHeightHeading,
335
+ },
336
+ h3: {
337
+ fontFamily: theme.seedTypography.fontHeading,
338
+ fontSize: 16 * theme.seedTypography.fontScaleH3,
339
+ fontWeight: 600,
340
+ color: theme.palette.headingColor,
341
+ lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
342
+ },
343
+ h4: {
344
+ fontFamily: theme.seedTypography.fontHeading,
345
+ fontSize: 16 * theme.seedTypography.fontScaleH4,
346
+ fontWeight: 600,
347
+ color: theme.palette.textMuted,
348
+ lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
349
+ },
350
+ h5: {
351
+ fontFamily: theme.seedTypography.fontHeading,
352
+ fontSize: 16 * theme.seedTypography.fontScaleH5,
353
+ fontWeight: 500,
354
+ color: theme.palette.textMuted,
355
+ lineHeight: theme.seedTypography.lineHeightBody - 0.25,
356
+ },
357
+ h6: {
358
+ fontFamily: theme.seedTypography.fontHeading,
359
+ fontSize: 16 * theme.seedTypography.fontScaleH6,
360
+ fontWeight: 500,
361
+ color: theme.palette.textMuted,
362
+ lineHeight: theme.seedTypography.lineHeightBody - 0.2,
363
+ },
364
+ },
365
+ body: {
366
+ fontFamily: theme.seedTypography.fontBody,
367
+ fontSize: parseInt(theme.typography.baseFontSize) || 16,
368
+ fontWeight: 400,
369
+ color: theme.palette.textColor,
370
+ lineHeight: theme.typography.baseLineHeight,
371
+ },
372
+ blockquote: {
373
+ fontFamily: theme.seedTypography.fontBody,
374
+ fontSize: Math.max(13, (parseInt(theme.typography.baseFontSize) || 16) - 2),
375
+ fontWeight: 400,
376
+ color: theme.palette.textMuted,
377
+ lineHeight: theme.typography.baseLineHeight - 0.15,
378
+ borderLeftColor: theme.palette.blockquoteBorder,
379
+ bgColor: theme.palette.blockquoteBg,
380
+ },
381
+ codeBlock: {
382
+ fontFamily: theme.typography.codeFontFamily,
383
+ fontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
384
+ fontWeight: 400,
385
+ color: theme.palette.codeBlockText || theme.palette.textColor,
386
+ lineHeight: theme.seedTypography.lineHeightCode,
387
+ bgColor: theme.palette.codeBlockBg,
388
+ },
389
+ inlineCode: {
390
+ fontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
391
+ color: theme.palette.accentColor,
392
+ bgColor: theme.palette.codeBg,
393
+ },
394
+ table: {
395
+ headerFontFamily: theme.seedTypography.fontHeading,
396
+ headerFontSize: Math.max(12, (parseInt(theme.typography.baseFontSize) || 16) - 3),
397
+ headerFontWeight: 700,
398
+ headerColor: "#ffffff",
399
+ headerBgColor: theme.palette.tableHeaderBg,
400
+ headerLineHeight: 1.4,
401
+
402
+ cellFontFamily: theme.seedTypography.fontBody,
403
+ cellFontSize: Math.max(11, (parseInt(theme.typography.baseFontSize) || 16) - 4),
404
+ cellColor: theme.palette.textColor,
405
+ cellBgColor: theme.palette.bgColor,
406
+ cellLineHeight: 1.5,
407
+ cellPadding: "8px 12px",
408
+ borderColor: theme.palette.borderColor,
409
+ borderWidth: "1px",
410
+ },
411
+
412
+ accentColor: theme.palette.accentColor,
413
+ mutedTextColor: theme.palette.textMuted,
414
+ selectionBg: theme.palette.selectionBg,
415
+ };
416
+ }
417
+ }