@bytechain.cn/colamd-themes 0.3.2 → 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 -37
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -36
  9. package/CHANGELOG.md +318 -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/bytechain.cn-colamd-themes-0.5.0.tgz +0 -0
  15. package/dist/cli.js +36 -55
  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 +64 -84
  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 -178
  56. package/src/cli.ts +474 -487
  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 -359
  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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared document paradigm utilities used by all extractors.
3
+ * Extracted from url-extractor, docx-extractor, and pdf-extractor
4
+ * to eliminate code duplication.
5
+ */
6
+
7
+ import type { ThemeStyle } from "../models.js";
8
+ import type { DocumentStyleParadigm } from "../docx-style-paradigm.js";
9
+ import {
10
+ ACADEMIC_PAPER_DEFAULTS,
11
+ validateDocumentStyle,
12
+ } from "../docx-style-paradigm.js";
13
+
14
+ export function themeToParadigm(theme: ThemeStyle): DocumentStyleParadigm {
15
+ const baseFontSize = parseInt(theme.typography.baseFontSize) || 16;
16
+
17
+ return {
18
+ pageBackground: theme.palette.bgColor,
19
+ headings: {
20
+ h1: {
21
+ fontFamily: theme.seedTypography.fontHeading,
22
+ fontSize: 16 * theme.seedTypography.fontScaleH1,
23
+ fontWeight: theme.typography.headingWeight,
24
+ color: theme.palette.headingColor,
25
+ lineHeight: theme.seedTypography.lineHeightHeading,
26
+ },
27
+ h2: {
28
+ fontFamily: theme.seedTypography.fontHeading,
29
+ fontSize: 16 * theme.seedTypography.fontScaleH2,
30
+ fontWeight: Math.max(600, theme.typography.headingWeight - 100),
31
+ color: theme.palette.headingColor,
32
+ lineHeight: theme.seedTypography.lineHeightHeading,
33
+ },
34
+ h3: {
35
+ fontFamily: theme.seedTypography.fontHeading,
36
+ fontSize: 16 * theme.seedTypography.fontScaleH3,
37
+ fontWeight: 600,
38
+ color: theme.palette.headingColor,
39
+ lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
40
+ },
41
+ h4: {
42
+ fontFamily: theme.seedTypography.fontHeading,
43
+ fontSize: 16 * theme.seedTypography.fontScaleH4,
44
+ fontWeight: 600,
45
+ color: theme.palette.textMuted,
46
+ lineHeight: theme.seedTypography.lineHeightHeading + 0.05,
47
+ },
48
+ h5: {
49
+ fontFamily: theme.seedTypography.fontHeading,
50
+ fontSize: 16 * theme.seedTypography.fontScaleH5,
51
+ fontWeight: 500,
52
+ color: theme.palette.textMuted,
53
+ lineHeight: theme.seedTypography.lineHeightBody - 0.25,
54
+ },
55
+ h6: {
56
+ fontFamily: theme.seedTypography.fontHeading,
57
+ fontSize: 16 * theme.seedTypography.fontScaleH6,
58
+ fontWeight: 500,
59
+ color: theme.palette.textMuted,
60
+ lineHeight: theme.seedTypography.lineHeightBody - 0.2,
61
+ },
62
+ },
63
+ body: {
64
+ fontFamily: theme.seedTypography.fontBody,
65
+ fontSize: baseFontSize,
66
+ fontWeight: 400,
67
+ color: theme.palette.textColor,
68
+ lineHeight: theme.typography.baseLineHeight,
69
+ },
70
+ blockquote: {
71
+ fontFamily: theme.seedTypography.fontBody,
72
+ fontSize: Math.max(13, baseFontSize - 2),
73
+ fontWeight: 400,
74
+ color: theme.palette.textMuted,
75
+ lineHeight: theme.typography.baseLineHeight - 0.15,
76
+ borderLeftColor: theme.palette.blockquoteBorder,
77
+ bgColor: theme.palette.blockquoteBg,
78
+ },
79
+ codeBlock: {
80
+ fontFamily: theme.typography.codeFontFamily,
81
+ fontSize: Math.max(12, baseFontSize - 3),
82
+ fontWeight: 400,
83
+ color: theme.palette.codeBlockText || theme.palette.textColor,
84
+ lineHeight: theme.seedTypography.lineHeightCode,
85
+ bgColor: theme.palette.codeBlockBg,
86
+ },
87
+ inlineCode: {
88
+ fontSize: Math.max(12, baseFontSize - 3),
89
+ color: theme.palette.accentColor,
90
+ bgColor: theme.palette.codeBg,
91
+ },
92
+ table: {
93
+ headerFontFamily: theme.seedTypography.fontHeading,
94
+ headerFontSize: Math.max(12, baseFontSize - 3),
95
+ headerFontWeight: 700,
96
+ headerColor: "#ffffff",
97
+ headerBgColor: theme.palette.tableHeaderBg,
98
+ headerLineHeight: 1.4,
99
+ cellFontFamily: theme.seedTypography.fontBody,
100
+ cellFontSize: Math.max(11, baseFontSize - 4),
101
+ cellColor: theme.palette.textColor,
102
+ cellBgColor: theme.palette.bgColor,
103
+ cellLineHeight: 1.5,
104
+ cellPadding: "8px 12px",
105
+ borderColor: theme.palette.borderColor,
106
+ borderWidth: "1px",
107
+ },
108
+ accentColor: theme.palette.accentColor,
109
+ mutedTextColor: theme.palette.textMuted,
110
+ selectionBg: theme.palette.selectionBg,
111
+ };
112
+ }
113
+
114
+ export function logExtractorWarnings(extractorName: string, issues: { element: string; message: string }[]): void {
115
+ for (const issue of issues) {
116
+ console.warn(`[${extractorName}] ${issue.element}: ${issue.message}`);
117
+ }
118
+ }
package/src/validators.ts CHANGED
@@ -1,176 +1,298 @@
1
- /**
2
- * Paradigm validation engine — checks SeedPalette against v3.0 design constraints.
3
- * Based on theme-paradigm.md Section 2.
4
- */
5
-
6
- import { contrastRatio, hslSaturation, hslLightness, hslHue, sameHueAxis, isLightMode } from "./contrast.js";
7
-
8
- export interface ValidationIssue {
9
- ruleId: string;
10
- level: "MUST" | "MUST_NOT" | "SHOULD" | "SHOULD_NOT";
11
- message: string;
12
- fix?: string;
13
- }
14
-
15
- export interface SeedPalette {
16
- accent: string;
17
- accentLight: string;
18
- accentDark: string;
19
- surface: string;
20
- panel: string;
21
- panelAlt: string;
22
- ink: string;
23
- inkMuted: string;
24
- inkDim: string;
25
- border: string;
26
- borderStrong: string;
27
- }
28
-
29
- export function validateSeedPalette(palette: SeedPalette): ValidationIssue[] {
30
- const issues: ValidationIssue[] = [];
31
- const light = isLightMode(palette.surface);
32
-
33
- // CR-01: ink vs surface ≥ 7:1
34
- const crInk = contrastRatio(palette.ink, palette.surface);
35
- if (crInk < 7.0) {
36
- issues.push({
37
- ruleId: "CR-01",
38
- level: "MUST",
39
- message: `正文-底色对比度 ${crInk.toFixed(1)}:1 < 7:1 (WCAG AAA)`,
40
- fix: `建议加深 ink 或调浅 surface`,
41
- });
42
- }
43
-
44
- // CR-02: inkMuted vs surface ≥ 4.5:1
45
- const crMuted = contrastRatio(palette.inkMuted, palette.surface);
46
- if (crMuted < 4.5) {
47
- issues.push({
48
- ruleId: "CR-02",
49
- level: "MUST",
50
- message: `次要文字-底色对比度 ${crMuted.toFixed(1)}:1 < 4.5:1 (WCAG AA)`,
51
- });
52
- }
53
-
54
- // CR-04: accent vs surface ≥ 4.5:1
55
- const crAccent = contrastRatio(palette.accent, palette.surface);
56
- if (crAccent < 4.5) {
57
- issues.push({
58
- ruleId: "CR-04",
59
- level: "MUST",
60
- message: `点缀色-底色对比度 ${crAccent.toFixed(1)}:1 < 4.5:1`,
61
- });
62
- }
63
-
64
- // SL-01: surface saturation ≤ 0.05
65
- const sSurface = hslSaturation(palette.surface);
66
- if (sSurface > 0.05) {
67
- issues.push({
68
- ruleId: "SL-01",
69
- level: "MUST",
70
- message: `底色饱和度 ${(sSurface * 100).toFixed(1)}% > 5%,页面会偏色`,
71
- });
72
- }
73
-
74
- // SL-02: panel/panelAlt saturation ≤ 0.10
75
- for (const [name, hex] of [["panel", palette.panel], ["panelAlt", palette.panelAlt]] as const) {
76
- const s = hslSaturation(hex);
77
- if (s > 0.10) {
78
- issues.push({
79
- ruleId: "SL-02",
80
- level: "MUST_NOT",
81
- message: `表面色 ${name} 饱和度 ${(s * 100).toFixed(1)}% > 10%`,
82
- });
83
- }
84
- }
85
-
86
- // SL-03: surface/panel/panelAlt same hue axis
87
- if (!sameHueAxis(palette.surface, palette.panel, 5) || !sameHueAxis(palette.surface, palette.panelAlt, 5)) {
88
- issues.push({
89
- ruleId: "SL-03",
90
- level: "SHOULD",
91
- message: "三层表面色应保持相同色相轴(H 偏差 ≤ 5°)",
92
- });
93
- }
94
-
95
- // SL-04: L(surface) > L(panel) > L(panelAlt) for light
96
- const lSurface = hslLightness(palette.surface);
97
- const lPanel = hslLightness(palette.panel);
98
- const lPanelAlt = hslLightness(palette.panelAlt);
99
- if (light) {
100
- if (!(lSurface > lPanel && lPanel > lPanelAlt)) {
101
- issues.push({
102
- ruleId: "SL-04",
103
- level: "MUST",
104
- message: `浅色主题要求 L(surface) > L(panel) > L(panelAlt)`,
105
- });
106
- }
107
- } else {
108
- if (!(lSurface < lPanel && lPanel < lPanelAlt)) {
109
- issues.push({
110
- ruleId: "SL-04",
111
- level: "MUST",
112
- message: `深色主题要求 L(surface) < L(panel) < L(panelAlt)`,
113
- });
114
- }
115
- }
116
-
117
- // NT-01: ink/inkMuted/inkDim same hue axis
118
- if (!sameHueAxis(palette.ink, palette.inkMuted, 3) || !sameHueAxis(palette.ink, palette.inkDim, 3)) {
119
- issues.push({
120
- ruleId: "NT-01",
121
- level: "MUST",
122
- message: "文字色必须位于同一色相轴(H 偏差 ≤ 3°)",
123
- });
124
- }
125
-
126
- // NT-02: border vs panel lightness diff ≥ 0.08
127
- const lBorder = hslLightness(palette.border);
128
- if (Math.abs(lBorder - lPanel) < 0.08) {
129
- issues.push({
130
- ruleId: "NT-02",
131
- level: "MUST",
132
- message: `边框色与 panel 明度差 ${Math.abs(lBorder - lPanel).toFixed(3)} < 0.08,边框不可见`,
133
- });
134
- }
135
-
136
- // NT-03: border should be neutral gray
137
- const sBorder = hslSaturation(palette.border);
138
- if (sBorder > 0.05) {
139
- issues.push({
140
- ruleId: "NT-03",
141
- level: "SHOULD",
142
- message: `边框色饱和度 ${(sBorder * 100).toFixed(1)}% > 5%,应使用中性灰`,
143
- });
144
- }
145
-
146
- // HR-01: accent saturation ≥ 0.30 for light themes
147
- const sAccent = hslSaturation(palette.accent);
148
- if (light && sAccent < 0.30) {
149
- issues.push({
150
- ruleId: "HR-01",
151
- level: "SHOULD",
152
- message: `点缀色饱和度 ${(sAccent * 100).toFixed(1)}% < 30%,建议选暖色或冷色`,
153
- });
154
- }
155
-
156
- return issues;
157
- }
158
-
159
- /** Summarize validation results. */
160
- export function summarizeIssues(issues: ValidationIssue[]): string {
161
- const errors = issues.filter((i) => i.level === "MUST" || i.level === "MUST_NOT");
162
- const warnings = issues.filter((i) => i.level === "SHOULD" || i.level === "SHOULD_NOT");
163
- const lines: string[] = [];
164
- if (errors.length > 0) {
165
- lines.push(`ERRORS (${errors.length}):`);
166
- for (const e of errors) lines.push(` [${e.ruleId}] ${e.message}`);
167
- }
168
- if (warnings.length > 0) {
169
- lines.push(`WARNINGS (${warnings.length}):`);
170
- for (const w of warnings) lines.push(` [${w.ruleId}] ${w.message}`);
171
- }
172
- if (errors.length === 0 && warnings.length === 0) {
173
- lines.push("All validations passed.");
174
- }
175
- return lines.join("\n");
176
- }
1
+ /**
2
+ * Paradigm validation engine — checks SeedPalette against v4.0 design constraints
3
+ * and scans generated CSS for the ColaMD 2.7.7 theme contract.
4
+ * Based on theme-paradigm.md Section 2.
5
+ */
6
+
7
+ import { parse, walk, generate } from "css-tree";
8
+ import { contrastRatio, hslSaturation, hslLightness, hslHue, sameHueAxis, isLightMode } from "./contrast.js";
9
+
10
+ export interface ValidationIssue {
11
+ ruleId: string;
12
+ level: "MUST" | "MUST_NOT" | "SHOULD" | "SHOULD_NOT";
13
+ message: string;
14
+ fix?: string;
15
+ }
16
+
17
+ export interface SeedPalette {
18
+ accent: string;
19
+ accentLight: string;
20
+ accentDark: string;
21
+ surface: string;
22
+ panel: string;
23
+ panelAlt: string;
24
+ ink: string;
25
+ inkMuted: string;
26
+ inkDim: string;
27
+ border: string;
28
+ borderStrong: string;
29
+ }
30
+
31
+ export function validateSeedPalette(palette: SeedPalette): ValidationIssue[] {
32
+ const issues: ValidationIssue[] = [];
33
+ const light = isLightMode(palette.surface);
34
+
35
+ // CR-01: ink vs surface ≥ 7:1
36
+ const crInk = contrastRatio(palette.ink, palette.surface);
37
+ if (crInk < 7.0) {
38
+ issues.push({
39
+ ruleId: "CR-01",
40
+ level: "MUST",
41
+ message: `正文-底色对比度 ${crInk.toFixed(1)}:1 < 7:1 (WCAG AAA)`,
42
+ fix: `建议加深 ink 或调浅 surface`,
43
+ });
44
+ }
45
+
46
+ // CR-02: inkMuted vs surface ≥ 4.5:1
47
+ const crMuted = contrastRatio(palette.inkMuted, palette.surface);
48
+ if (crMuted < 4.5) {
49
+ issues.push({
50
+ ruleId: "CR-02",
51
+ level: "MUST",
52
+ message: `次要文字-底色对比度 ${crMuted.toFixed(1)}:1 < 4.5:1 (WCAG AA)`,
53
+ });
54
+ }
55
+
56
+ // CR-04: accent vs surface ≥ 4.5:1
57
+ const crAccent = contrastRatio(palette.accent, palette.surface);
58
+ if (crAccent < 4.5) {
59
+ issues.push({
60
+ ruleId: "CR-04",
61
+ level: "MUST",
62
+ message: `点缀色-底色对比度 ${crAccent.toFixed(1)}:1 < 4.5:1`,
63
+ });
64
+ }
65
+
66
+ // SL-01: surface saturation ≤ 0.05
67
+ const sSurface = hslSaturation(palette.surface);
68
+ if (sSurface > 0.05) {
69
+ issues.push({
70
+ ruleId: "SL-01",
71
+ level: "MUST",
72
+ message: `底色饱和度 ${(sSurface * 100).toFixed(1)}% > 5%,页面会偏色`,
73
+ });
74
+ }
75
+
76
+ // SL-02: panel/panelAlt saturation ≤ 0.10
77
+ for (const [name, hex] of [["panel", palette.panel], ["panelAlt", palette.panelAlt]] as const) {
78
+ const s = hslSaturation(hex);
79
+ if (s > 0.10) {
80
+ issues.push({
81
+ ruleId: "SL-02",
82
+ level: "MUST_NOT",
83
+ message: `表面色 ${name} 饱和度 ${(s * 100).toFixed(1)}% > 10%`,
84
+ });
85
+ }
86
+ }
87
+
88
+ // SL-03: surface/panel/panelAlt same hue axis
89
+ if (!sameHueAxis(palette.surface, palette.panel, 5) || !sameHueAxis(palette.surface, palette.panelAlt, 5)) {
90
+ issues.push({
91
+ ruleId: "SL-03",
92
+ level: "SHOULD",
93
+ message: "三层表面色应保持相同色相轴(H 偏差 ≤ 5°)",
94
+ });
95
+ }
96
+
97
+ // SL-04: L(surface) > L(panel) > L(panelAlt) for light
98
+ const lSurface = hslLightness(palette.surface);
99
+ const lPanel = hslLightness(palette.panel);
100
+ const lPanelAlt = hslLightness(palette.panelAlt);
101
+ if (light) {
102
+ if (!(lSurface > lPanel && lPanel > lPanelAlt)) {
103
+ issues.push({
104
+ ruleId: "SL-04",
105
+ level: "MUST",
106
+ message: `浅色主题要求 L(surface) > L(panel) > L(panelAlt)`,
107
+ });
108
+ }
109
+ } else {
110
+ if (!(lSurface < lPanel && lPanel < lPanelAlt)) {
111
+ issues.push({
112
+ ruleId: "SL-04",
113
+ level: "MUST",
114
+ message: `深色主题要求 L(surface) < L(panel) < L(panelAlt)`,
115
+ });
116
+ }
117
+ }
118
+
119
+ // NT-01: ink/inkMuted/inkDim same hue axis
120
+ if (!sameHueAxis(palette.ink, palette.inkMuted, 3) || !sameHueAxis(palette.ink, palette.inkDim, 3)) {
121
+ issues.push({
122
+ ruleId: "NT-01",
123
+ level: "MUST",
124
+ message: "文字色必须位于同一色相轴(H 偏差 ≤ 3°)",
125
+ });
126
+ }
127
+
128
+ // NT-02: border vs panel lightness diff ≥ 0.08
129
+ const lBorder = hslLightness(palette.border);
130
+ if (Math.abs(lBorder - lPanel) < 0.08) {
131
+ issues.push({
132
+ ruleId: "NT-02",
133
+ level: "MUST",
134
+ message: `边框色与 panel 明度差 ${Math.abs(lBorder - lPanel).toFixed(3)} < 0.08,边框不可见`,
135
+ });
136
+ }
137
+
138
+ // NT-03: border should be neutral gray
139
+ const sBorder = hslSaturation(palette.border);
140
+ if (sBorder > 0.05) {
141
+ issues.push({
142
+ ruleId: "NT-03",
143
+ level: "SHOULD",
144
+ message: `边框色饱和度 ${(sBorder * 100).toFixed(1)}% > 5%,应使用中性灰`,
145
+ });
146
+ }
147
+
148
+ // HR-01: accent saturation ≥ 0.30 for light themes
149
+ const sAccent = hslSaturation(palette.accent);
150
+ if (light && sAccent < 0.30) {
151
+ issues.push({
152
+ ruleId: "HR-01",
153
+ level: "SHOULD",
154
+ message: `点缀色饱和度 ${(sAccent * 100).toFixed(1)}% < 30%,建议选暖色或冷色`,
155
+ });
156
+ }
157
+
158
+ return issues;
159
+ }
160
+
161
+ // ═══════════════════════════════════════════════════════════
162
+ // CSS contract validation (ColaMD 2.7.7)
163
+ // ═══════════════════════════════════════════════════════════
164
+
165
+ /** 2.7 编辑器不再消费的变量:留着只会让人照抄。 */
166
+ const DEAD_VARIABLES = new Set([
167
+ "--surface-color", "--surface-alt", "--text-dim", "--heading-color",
168
+ "--accent-color", "--accent-dark", "--accent-light", "--hr-color",
169
+ "--code-color", "--code-block-color", "--table-border",
170
+ "--content-width", "--font-size-root", "--font-heading", "--font-code",
171
+ "--font-mermaid", "--font-size-mermaid",
172
+ ]);
173
+
174
+ /** 前缀匹配的死变量族(Mermaid 20 变量、圆角、字阶、行高、滚动条)。 */
175
+ const DEAD_VARIABLE_PREFIXES = [
176
+ "--mermaid-", "--radius-", "--font-scale-", "--line-height-", "--scrollbar-thumb",
177
+ ];
178
+
179
+ /** 旧编辑器 DOM 的选择器:在 CodeMirror 6 里命中不到任何元素。 */
180
+ const DEAD_SELECTOR_PATTERNS: Array<{ re: RegExp; label: string; fix: string }> = [
181
+ { re: /(^|[\s>+~,])#write(?![-\w])/, label: "#write", fix: "改用 #editor .cm-content" },
182
+ { re: /\.ProseMirror\b/, label: ".ProseMirror", fix: "改用 #editor .cm-content .cm-md-*" },
183
+ { re: /\.theme-custom\b/, label: "body.theme-custom", fix: "自定义主题变量写在 :root" },
184
+ { re: /\.mermaid-(block|preview)\b/, label: ".mermaid-*", fix: "图表用 .cm-md-mermaid 与 --pele-*" },
185
+ ];
186
+
187
+ /** 引用块/代码块在 CM6 里一行一个 div,纵向 padding 会按行累加。 */
188
+ function hasVerticalPadding(rule: { block: { children: { forEach: (cb: (node: any) => void) => void } } }): boolean {
189
+ let found = false;
190
+ rule.block.children.forEach((node: any) => {
191
+ if (node.type !== "Declaration" || node.property.startsWith("--")) return;
192
+ if (node.property === "padding") {
193
+ const values = generate(node.value).trim().split(/\s+/);
194
+ // 1~2 个值:第一个是纵向;3 个值:第二个;4 个值:第一个与第三个。
195
+ const vertical = values.length === 1 ? values[0]
196
+ : values.length === 2 ? values[0]
197
+ : values.length === 3 ? values[1] : `${values[0]} ${values[2]}`;
198
+ if (vertical.split(/\s+/).some((v) => !/^0(px|em|rem|%)?$/.test(v))) found = true;
199
+ } else if (node.property === "padding-top" || node.property === "padding-bottom") {
200
+ const value = generate(node.value).trim();
201
+ if (!/^0(px|em|rem|%)?$/.test(value)) found = true;
202
+ }
203
+ });
204
+ return found;
205
+ }
206
+
207
+ /** 校验生成的 CSS 是否符合 ColaMD 2.7.7 契约。 */
208
+ export function validateThemeCss(css: string): ValidationIssue[] {
209
+ const issues: ValidationIssue[] = [];
210
+ const ast = parse(css, { positions: false });
211
+ let hasRootBlock = false;
212
+
213
+ walk(ast, (node) => {
214
+ if (node.type === "Declaration" && node.property.startsWith("--")) {
215
+ const name = node.property;
216
+ const dead = DEAD_VARIABLES.has(name)
217
+ || DEAD_VARIABLE_PREFIXES.some((prefix) => name.startsWith(prefix));
218
+ if (dead) {
219
+ issues.push({
220
+ ruleId: "CSS-01",
221
+ level: "MUST_NOT",
222
+ message: `变量 ${name} 在 ColaMD 2.7.7 已不再消费`,
223
+ fix: "改用活变量清单(--bg-color / --text-secondary / --pele-* 等)",
224
+ });
225
+ }
226
+ }
227
+
228
+ if (node.type === "Rule" && node.prelude.type === "SelectorList") {
229
+ const selector = generate(node.prelude);
230
+ if (/(^|,)\s*:root\b/.test(selector)) hasRootBlock = true;
231
+
232
+ for (const dead of DEAD_SELECTOR_PATTERNS) {
233
+ if (dead.re.test(selector)) {
234
+ issues.push({
235
+ ruleId: "CSS-02",
236
+ level: "MUST_NOT",
237
+ message: `选择器 ${dead.label} 在 CodeMirror 6 中命中不到元素(${selector})`,
238
+ fix: dead.fix,
239
+ });
240
+ }
241
+ }
242
+
243
+ if (/\.cm-md-(blockquote|codeblock)\b/.test(selector) && hasVerticalPadding(node)) {
244
+ issues.push({
245
+ ruleId: "CSS-03",
246
+ level: "MUST_NOT",
247
+ message: `引用/代码块不能设置纵向 padding(每行一个 div,会按行累加):${selector}`,
248
+ fix: "只设 padding-left / padding-right",
249
+ });
250
+ }
251
+
252
+ walk(node.block, (child) => {
253
+ if (child.type === "Declaration" && child.value.type === "Value") {
254
+ walk(child.value, (valueNode) => {
255
+ if (valueNode.type === "Dimension" && valueNode.unit === "pt") {
256
+ issues.push({
257
+ ruleId: "CSS-04",
258
+ level: "MUST_NOT",
259
+ message: `使用了 pt 单位:${child.property}: ${generate(child.value)}`,
260
+ fix: "统一换算为 px(1pt ≈ 1.333px)",
261
+ });
262
+ }
263
+ });
264
+ }
265
+ });
266
+ }
267
+ });
268
+
269
+ if (!hasRootBlock) {
270
+ issues.push({
271
+ ruleId: "CSS-05",
272
+ level: "MUST",
273
+ message: "缺少 :root 根块,主题变量无法被继承",
274
+ fix: "把变量块写到 :root 上",
275
+ });
276
+ }
277
+
278
+ return issues;
279
+ }
280
+
281
+ /** Summarize validation results. */
282
+ export function summarizeIssues(issues: ValidationIssue[]): string {
283
+ const errors = issues.filter((i) => i.level === "MUST" || i.level === "MUST_NOT");
284
+ const warnings = issues.filter((i) => i.level === "SHOULD" || i.level === "SHOULD_NOT");
285
+ const lines: string[] = [];
286
+ if (errors.length > 0) {
287
+ lines.push(`ERRORS (${errors.length}):`);
288
+ for (const e of errors) lines.push(` [${e.ruleId}] ${e.message}`);
289
+ }
290
+ if (warnings.length > 0) {
291
+ lines.push(`WARNINGS (${warnings.length}):`);
292
+ for (const w of warnings) lines.push(` [${w.ruleId}] ${w.message}`);
293
+ }
294
+ if (errors.length === 0 && warnings.length === 0) {
295
+ lines.push("All validations passed.");
296
+ }
297
+ return lines.join("\n");
298
+ }