@bytechain.cn/colamd-themes 0.3.1 → 0.5.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 (100) 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 -146
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -145
  9. package/CHANGELOG.md +318 -240
  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 -365
  14. package/bytechain.cn-colamd-themes-0.5.0.tgz +0 -0
  15. package/dist/cli.js +272 -32
  16. package/dist/cli.js.map +1 -1
  17. package/dist/color-utils.d.ts +1 -0
  18. package/dist/color-utils.js +25 -6
  19. package/dist/color-utils.js.map +1 -1
  20. package/dist/extractors/pdf-extractor.js +6 -1
  21. package/dist/extractors/pdf-extractor.js.map +1 -1
  22. package/dist/extractors/url-extractor.d.ts +0 -4
  23. package/dist/extractors/url-extractor.js +13 -126
  24. package/dist/extractors/url-extractor.js.map +1 -1
  25. package/dist/generator.d.ts +1 -1
  26. package/dist/generator.js +32 -11
  27. package/dist/generator.js.map +1 -1
  28. package/dist/models.d.ts +5 -4
  29. package/dist/models.js +2 -3
  30. package/dist/models.js.map +1 -1
  31. package/dist/renderer.d.ts +4 -2
  32. package/dist/renderer.js +68 -25
  33. package/dist/renderer.js.map +1 -1
  34. package/dist/templates/template.css +220 -498
  35. package/dist/templates/theme-paradigm.md +530 -658
  36. package/dist/templates/theme-template.hbs +256 -370
  37. package/dist/theme-store.js +19 -3
  38. package/dist/theme-store.js.map +1 -1
  39. package/dist/utils/document-paradigm.d.ts +12 -0
  40. package/dist/utils/document-paradigm.js +109 -0
  41. package/dist/utils/document-paradigm.js.map +1 -0
  42. package/dist/validators.d.ts +4 -1
  43. package/dist/validators.js +115 -1
  44. package/dist/validators.js.map +1 -1
  45. package/mcp-server/dist/index.d.ts +13 -0
  46. package/mcp-server/dist/index.d.ts.map +1 -0
  47. package/mcp-server/dist/index.js +0 -0
  48. package/mcp-server/dist/index.js.map +1 -0
  49. package/mcp-server/package-lock.json +1759 -0
  50. package/mcp-server/package.json +24 -0
  51. package/mcp-server/src/index.ts +0 -0
  52. package/mcp-server/tsconfig.json +19 -0
  53. package/package.json +54 -36
  54. package/skills/build-skills.sh +30 -5
  55. package/skills/colamd-themes/SKILL.md +180 -148
  56. package/src/cli.ts +474 -217
  57. package/src/color-systems.ts +289 -289
  58. package/src/color-utils.ts +137 -120
  59. package/src/contrast.ts +66 -66
  60. package/src/docx-style-paradigm.ts +811 -811
  61. package/src/export-cli.ts +356 -356
  62. package/src/extractors/base.ts +8 -8
  63. package/src/extractors/docx-extractor.ts +417 -417
  64. package/src/extractors/pdf-extractor.ts +533 -527
  65. package/src/extractors/url-extractor.ts +846 -981
  66. package/src/generator.ts +197 -169
  67. package/src/index.ts +29 -29
  68. package/src/models.ts +212 -212
  69. package/src/renderer.ts +332 -288
  70. package/src/templates/template.css +220 -498
  71. package/src/templates/theme-paradigm.md +530 -658
  72. package/src/templates/theme-template.hbs +256 -370
  73. package/src/theme-store.ts +185 -170
  74. package/src/types/pdf-parse.d.ts +12 -12
  75. package/src/utils/cli-utils.ts +84 -84
  76. package/src/utils/document-paradigm.ts +118 -0
  77. package/src/validators.ts +298 -176
  78. package/themes/academic-paper-pt.css +353 -1805
  79. package/themes/academic-paper.css +350 -1099
  80. package/themes/academic-style.css +342 -2430
  81. package/themes/colly.css +256 -370
  82. package/themes/elegant.css +288 -671
  83. package/themes/forest-ink.css +240 -664
  84. package/themes/guizang.css +250 -732
  85. package/themes/hermes-security.css +279 -0
  86. package/themes/meeting-minutes.css +256 -370
  87. package/themes/pixso-design.css +278 -1261
  88. package/themes/salvator-blog.css +256 -370
  89. package/themes/shimo-theme.css +256 -0
  90. package/themes/swiss-design.css +285 -746
  91. package/themes/template.css +220 -498
  92. package/themes/trae-blue.css +280 -737
  93. package/themes/word-contract.css +249 -0
  94. package/themes-preview/demo-content.md +187 -187
  95. package/tsconfig.json +18 -18
  96. package/src/mermaid-presets.ts +0 -142
  97. package/themes/swiss-design copy.css +0 -746
  98. package/themes-preview/colly-preview.html +0 -623
  99. package/themes-preview/meeting-minutes-preview.html +0 -625
  100. 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
+ }