@zzclub/pipeline 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +113 -8
  2. package/package.json +4 -4
  3. package/src/adapter-types.ts +21 -0
  4. package/src/adapters/builtin-markdown-renderer.ts +7 -0
  5. package/src/cli.ts +5 -1
  6. package/src/commands/ingest-handoff.ts +1 -21
  7. package/src/commands/prepare-finalize.ts +2 -1
  8. package/src/commands/prepare.ts +30 -19
  9. package/src/commands/publish.ts +2 -3
  10. package/src/commands/reconcile.ts +3 -5
  11. package/src/commands/render.ts +2 -1
  12. package/src/commands/republish.ts +2 -3
  13. package/src/commands/reset.ts +9 -1
  14. package/src/commands/review.ts +3 -2
  15. package/src/commands/sync-blog.ts +2 -1
  16. package/src/commands/wechat-export.test.ts +41 -0
  17. package/src/commands/wechat-export.ts +156 -20
  18. package/src/commands/wechat-preview.ts +160 -0
  19. package/src/config.ts +12 -0
  20. package/src/imgx/runtime.ts +31 -3
  21. package/src/logger.test.ts +56 -0
  22. package/src/logger.ts +178 -0
  23. package/src/plugins.ts +2 -0
  24. package/src/providers/blog.ts +1 -2
  25. package/src/providers/index.ts +35 -3
  26. package/src/publish-targets.ts +4 -5
  27. package/src/routes.ts +9 -1
  28. package/src/schema/config.ts +2 -0
  29. package/src/state.ts +24 -0
  30. package/src/task-manager.test.ts +57 -0
  31. package/src/task-manager.ts +23 -5
  32. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1 -302
  33. package/src/wechat-preview/assets/browser-dist/editor-export.js +500 -958
  34. package/src/wechat-preview/browser/assets.d.ts +9 -0
  35. package/src/wechat-preview/browser/editor-export.css +5 -326
  36. package/src/wechat-preview/browser/editor-export.ts +30 -65
  37. package/src/wechat-preview/bundle-stale.test.ts +52 -0
  38. package/src/wechat-preview/fixtures/all-nodes.md +68 -0
  39. package/src/wechat-preview/index.test.ts +17 -0
  40. package/src/wechat-preview/index.ts +424 -180
  41. package/src/wechat-preview/server/client.ts +164 -0
  42. package/src/wechat-preview/server/http.ts +229 -0
  43. package/src/wechat-preview/server/index.ts +33 -0
  44. package/src/wechat-preview/server/local-file.test.ts +52 -0
  45. package/src/wechat-preview/server/local-file.ts +116 -0
  46. package/src/wechat-preview/server/paths.ts +80 -0
  47. package/src/wechat-preview/server/registry.test.ts +60 -0
  48. package/src/wechat-preview/server/registry.ts +118 -0
  49. package/src/wechat-preview/server/singleton.ts +118 -0
  50. package/src/wechat-preview/server/types.ts +61 -0
  51. package/src/wechat-preview/server/ui.ts +324 -0
  52. package/src/wechat-preview/themes.ts +58 -45
  53. package/src/wechat-preview/wechat-export.integration.test.ts +94 -0
  54. package/src/wechat-preview/wechat-renderer.links.test.ts +44 -0
  55. package/src/wechat-preview/wechat-renderer.test.ts +50 -0
  56. package/src/wechat-preview/wechat-renderer.ts +698 -0
  57. package/src/wechat-preview.test.ts +25 -41
  58. package/src/workflow-materials.test.ts +52 -1
  59. package/src/workflow-materials.ts +12 -0
  60. package/src/workflow.test.ts +105 -5
  61. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  62. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  63. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  64. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  65. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  66. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  67. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  68. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  69. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  70. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  71. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  72. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  73. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  74. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  75. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  76. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  77. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  78. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  79. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  80. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  81. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  82. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  83. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  84. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  85. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  86. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  87. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  88. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  89. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  90. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  91. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  92. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  93. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  94. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  95. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  96. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  97. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  98. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  99. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  100. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  101. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  102. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  103. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  104. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  105. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  106. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  107. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  108. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  109. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  110. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  111. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  112. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  113. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  114. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  115. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  116. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  117. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  118. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  119. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  120. package/src/wechat-preview/assets/browser-dist/assets/style-vhHYSP8T.css +0 -1
  121. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  122. package/src/wechat-preview/wechat-formatter.test.ts +0 -287
  123. package/src/wechat-preview/wechat-formatter.ts +0 -1531
@@ -5,6 +5,10 @@ export interface WechatExportTheme {
5
5
  fontFamily: string;
6
6
  bodyColor: string;
7
7
  mutedColor: string;
8
+ /** Major section heading (H2); slightly deeper than body */
9
+ h2Color: string;
10
+ /** In-section heading (H3); between body and muted */
11
+ h3Color: string;
8
12
  primaryColor: string;
9
13
  dividerColor: string;
10
14
  blockquoteBorderColor: string;
@@ -38,16 +42,19 @@ function buildContainerStyle(options: {
38
42
  letterSpacing?: string;
39
43
  }): string {
40
44
  return [
41
- "padding: 14px 11px",
45
+ "max-width: 100%",
46
+ "margin: 0",
47
+ "padding: 10px 4px 30px",
42
48
  `background: ${options.background}`,
43
- "background-size: 20px 20px",
44
- "background-position: center center",
49
+ "box-sizing: border-box",
45
50
  `font-family: ${options.fontFamily}`,
46
51
  "font-size: 16px",
47
52
  `color: ${options.color}`,
48
- "word-wrap: break-word",
49
- `line-height: ${options.lineHeight ?? "1.92"}`,
50
- `letter-spacing: ${options.letterSpacing ?? "0.03em"}`,
53
+ "overflow-wrap: break-word",
54
+ "word-break: break-word",
55
+ `line-height: ${options.lineHeight ?? "1.84"}`,
56
+ `letter-spacing: ${options.letterSpacing ?? "0.012em"}`,
57
+ "-webkit-text-size-adjust: 100%",
51
58
  ].join("; ");
52
59
  }
53
60
 
@@ -56,6 +63,8 @@ function buildFooterStyle(color: string): string {
56
63
  "margin-top: 32px",
57
64
  "text-align: center",
58
65
  "font-size: 12px",
66
+ "line-height: 1.6",
67
+ "letter-spacing: 0.08em",
59
68
  `color: ${color}`,
60
69
  ].join("; ");
61
70
  }
@@ -74,36 +83,38 @@ const THEMES: Record<string, WechatPreviewTheme> = {
74
83
  "--muted-foreground": "215 8% 42%",
75
84
  "--border": "220 10% 86%",
76
85
  "--brand": "#ca6093",
77
- "--brand-soft": "#d4789e",
86
+ "--brand-soft": "#ca6093",
78
87
  "--brand-bg": "rgba(202, 96, 147, 0.08)",
79
- "--brand-bg-deep": "rgba(202, 96, 147, 0.14)",
80
- "--text": "#202124",
81
- "--text-soft": "#5f6368",
82
- "--text-mid": "#3c4043",
83
- "--divider": "#dadce0",
88
+ "--brand-bg-deep": "rgba(202, 96, 147, 0.12)",
89
+ "--text": "#292526",
90
+ "--text-soft": "#6f696b",
91
+ "--text-mid": "#4d484a",
92
+ "--divider": "#e2dcdf",
84
93
  "--bg-page": "#ffffff",
85
- "--bg-warm": "#f8f9fa",
94
+ "--bg-warm": "#faf7f8",
86
95
  },
87
96
  exportTheme: {
88
97
  containerStyle: buildContainerStyle({
89
98
  background: "#ffffff",
90
- color: "#202124",
99
+ color: "#292526",
91
100
  fontFamily:
92
- "'SweiCurveLeg', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
93
- lineHeight: "1.92",
94
- letterSpacing: "0.03em",
101
+ "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif",
102
+ lineHeight: "1.84",
103
+ letterSpacing: "0.012em",
95
104
  }),
96
105
  footerText: "公众号 · 早早集市",
97
- footerStyle: buildFooterStyle("#8a8f98"),
106
+ footerStyle: buildFooterStyle("#716a6d"),
98
107
  fontFamily:
99
- "'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
100
- bodyColor: "#202124",
101
- mutedColor: "#5f6368",
102
- primaryColor: "#ca6093",
103
- dividerColor: "#dadce0",
108
+ "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif",
109
+ bodyColor: "#292526",
110
+ mutedColor: "#6f696b",
111
+ h2Color: "#1f1b1c",
112
+ h3Color: "#5c5658",
113
+ primaryColor: "#a94473",
114
+ dividerColor: "#e2dcdf",
104
115
  blockquoteBorderColor: "#ca6093",
105
- bodyLineHeight: "1.92",
106
- bodyLetterSpacing: "0.03em",
116
+ bodyLineHeight: "1.84",
117
+ bodyLetterSpacing: "0.012em",
107
118
  },
108
119
  },
109
120
  ancientone: {
@@ -117,36 +128,38 @@ const THEMES: Record<string, WechatPreviewTheme> = {
117
128
  "--accent-foreground": "337 28% 22%",
118
129
  "--muted": "338 25% 97%",
119
130
  "--border": "337 20% 88%",
120
- "--brand": "#ca6093",
121
- "--brand-soft": "#d4789e",
122
- "--brand-bg": "rgba(202, 96, 147, 0.08)",
123
- "--brand-bg-deep": "rgba(202, 96, 147, 0.14)",
124
- "--text": "#2c2c2c",
125
- "--text-soft": "#8c8c8c",
126
- "--text-mid": "#5c5c5c",
127
- "--divider": "#d8d0d3",
131
+ "--brand": "#b86f86",
132
+ "--brand-soft": "#b86f86",
133
+ "--brand-bg": "rgba(184, 111, 134, 0.08)",
134
+ "--brand-bg-deep": "rgba(184, 111, 134, 0.12)",
135
+ "--text": "#30292b",
136
+ "--text-soft": "#756b6e",
137
+ "--text-mid": "#534a4d",
138
+ "--divider": "#dfd7da",
128
139
  "--bg-page": "#fefefe",
129
- "--bg-warm": "#faf8f9",
140
+ "--bg-warm": "#faf7f8",
130
141
  },
131
142
  exportTheme: {
132
143
  containerStyle: buildContainerStyle({
133
144
  background: "#fefefe",
134
- color: "#2c2c2c",
145
+ color: "#30292b",
135
146
  fontFamily:
136
- "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, 'SF Pro Text', 'Noto Sans SC', sans-serif",
137
- lineHeight: "2.0",
147
+ "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif",
148
+ lineHeight: "1.85",
138
149
  letterSpacing: "0.015em",
139
150
  }),
140
151
  footerText: "公众号 · 古一软件",
141
- footerStyle: buildFooterStyle("#8c8c8c"),
152
+ footerStyle: buildFooterStyle("#756b6e"),
142
153
  fontFamily:
143
- "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, 'SF Pro Text', 'Noto Sans SC', sans-serif",
144
- bodyColor: "#2c2c2c",
145
- mutedColor: "#8c8c8c",
146
- primaryColor: "#ca6093",
147
- dividerColor: "#d8d0d3",
148
- blockquoteBorderColor: "#ca6093",
149
- bodyLineHeight: "2.0",
154
+ "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif",
155
+ bodyColor: "#30292b",
156
+ mutedColor: "#756b6e",
157
+ h2Color: "#241f21",
158
+ h3Color: "#61585b",
159
+ primaryColor: "#8f4d63",
160
+ dividerColor: "#dfd7da",
161
+ blockquoteBorderColor: "#b86f86",
162
+ bodyLineHeight: "1.85",
150
163
  bodyLetterSpacing: "0.015em",
151
164
  },
152
165
  },
@@ -0,0 +1,94 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { findChrome } from "../imgx/runtime";
7
+ import { exportMarkdownToWechatHtml } from "./index";
8
+
9
+ const fixturePath = join(
10
+ dirname(fileURLToPath(import.meta.url)),
11
+ "fixtures",
12
+ "all-nodes.md",
13
+ );
14
+
15
+ describe("semantic WeChat HTML export", () => {
16
+ let tempDir: string | null = null;
17
+
18
+ afterAll(async () => {
19
+ if (tempDir) await rm(tempDir, { recursive: true, force: true });
20
+ });
21
+
22
+ test.skipIf(findChrome() === null)("renders all nodes, cascades CSS, and emits a safe exact preview", async () => {
23
+ tempDir = await mkdtemp(join(tmpdir(), "zzhub-wechat-renderer-"));
24
+ const cssPath = join(tempDir, "custom.css");
25
+ const htmlPath = join(tempDir, "article.html");
26
+ const previewPath = join(tempDir, "preview.html");
27
+ await writeFile(cssPath, `
28
+ .milkdown .editor { --fixture-paragraph: #123456; }
29
+ .milkdown .editor p { color: var(--fixture-paragraph); }
30
+ .milkdown .editor h2 { color: #224466; display: grid; position: fixed; }
31
+ .milkdown .editor [data-wechat-node="inline-code"] { background-color: #abcdef; }
32
+ `, "utf-8");
33
+
34
+ const result = await exportMarkdownToWechatHtml({
35
+ markdownPath: fixturePath,
36
+ outPath: htmlPath,
37
+ previewShellOutPath: previewPath,
38
+ customCss: cssPath,
39
+ account: "default",
40
+ title: "全节点精确预览",
41
+ });
42
+ const html = await readFile(htmlPath, "utf-8");
43
+ const preview = await readFile(previewPath, "utf-8");
44
+
45
+ expect(result.html).toBe(html);
46
+ expect(html.length).toBeLessThan(65_400);
47
+ expect(html).toContain("一级标题:克制的中文编辑部");
48
+ expect(html).toContain("<blockquote");
49
+ expect(html).toContain("<table");
50
+ expect(html).toContain("<img ");
51
+ expect(html).toContain("图片说明:克制、清晰、适合移动端阅读");
52
+ expect(html).toContain("typescript</p><pre");
53
+ expect(html).toContain(" title: string;");
54
+ expect(html).toContain("white-space: pre");
55
+ expect(html).toContain("☑");
56
+ expect(html).toContain("☐");
57
+ expect(html).toContain("[1] 外部链接: https://example.com/reading-guide?from=wechat");
58
+ expect(html).toContain("[2] 资料链接: https://example.org/reference");
59
+ expect(html).toContain("相关链接");
60
+ expect(html).toMatch(/color: (?:#123456|rgb\(18, 52, 86\))/);
61
+ expect(html).toMatch(/color: (?:#224466|rgb\(34, 68, 102\))/);
62
+ expect(html).toMatch(/background-color: (?:#abcdef|rgb\(171, 205, 239\))/);
63
+ const inlineStyles = Array.from(
64
+ html.matchAll(/<[a-z][^>]*\sstyle="([^"]*)"/gi),
65
+ (match) => match[1] ?? "",
66
+ );
67
+ expect(inlineStyles.some((style) => style.includes("var("))).toBe(false);
68
+
69
+ expect(html).not.toMatch(/\sclass=/i);
70
+ expect(html).not.toContain("data-wechat-node");
71
+ expect(html).not.toContain(".milkdown");
72
+ expect(html).not.toContain(".cm-");
73
+ expect(html).not.toMatch(/<(?:script|style|iframe|object|embed|form|button)\b/i);
74
+ expect(html).not.toMatch(/<[^>]+\son[a-z]+=/i);
75
+ expect(inlineStyles.some((style) => (
76
+ /(?:^|;)\s*display:\s*(?:flex|grid)(?:;|$)/i.test(style) ||
77
+ /(?:^|;)\s*position\s*:/i.test(style)
78
+ ))).toBe(false);
79
+ expect(html).not.toMatch(/<(?:h[1-6]|figure|figcaption|div)\b/i);
80
+ expect(html).toContain("&lt;script&gt;alert('never')&lt;/script&gt;");
81
+
82
+ const tagNames = Array.from(html.matchAll(/<\/?([a-z][a-z0-9-]*)\b/gi), (match) => match[1]?.toLowerCase());
83
+ const allowedTags = new Set([
84
+ "section", "p", "blockquote", "ul", "ol", "li", "pre", "code",
85
+ "span", "strong", "em", "b", "i", "u", "s", "del", "a", "img",
86
+ "br", "hr", "table", "thead", "tbody", "tr", "th", "td",
87
+ ]);
88
+ expect(tagNames.every((tagName) => tagName !== undefined && allowedTags.has(tagName))).toBe(true);
89
+
90
+ expect(preview).toContain(`<main id="wechat-preview-content">${html}</main>`);
91
+ expect(preview).not.toContain("editor-export.js");
92
+ expect(preview).not.toContain("browser-dist");
93
+ }, 30_000);
94
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ normalizeReferenceUrl,
4
+ resolveLinkReference,
5
+ type WechatLinkReference,
6
+ } from "./wechat-renderer";
7
+
8
+ describe("normalizeReferenceUrl", () => {
9
+ test("treats trailing slash and hash as the same target", () => {
10
+ expect(normalizeReferenceUrl("https://Example.com/path/?q=1#frag")).toBe(
11
+ normalizeReferenceUrl("https://example.com/path?q=1"),
12
+ );
13
+ });
14
+ });
15
+
16
+ describe("resolveLinkReference", () => {
17
+ test("dedupes the same URL even when anchor text differs", () => {
18
+ const references: WechatLinkReference[] = [];
19
+
20
+ const first = resolveLinkReference(
21
+ "https://github.com/hugohe3/ppt-master",
22
+ "ppt-master",
23
+ references,
24
+ );
25
+ const second = resolveLinkReference(
26
+ "https://github.com/hugohe3/ppt-master/",
27
+ "开源仓库",
28
+ references,
29
+ );
30
+ const third = resolveLinkReference(
31
+ "https://example.com/other",
32
+ "其它",
33
+ references,
34
+ );
35
+
36
+ expect(first.index).toBe(1);
37
+ expect(second.index).toBe(1);
38
+ expect(second).toBe(first);
39
+ expect(third.index).toBe(2);
40
+ expect(references).toHaveLength(2);
41
+ expect(references[0]?.text).toBe("ppt-master");
42
+ expect(references[1]?.text).toBe("其它");
43
+ });
44
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getWechatPreviewTheme } from "./themes";
3
+ import {
4
+ buildWechatThemeCss,
5
+ createWechatRendererRegistry,
6
+ type WechatElementRenderer,
7
+ } from "./wechat-renderer";
8
+
9
+ describe("wechat element renderer registry", () => {
10
+ test("registers every built-in semantic element family in pipeline order", () => {
11
+ expect(createWechatRendererRegistry().map((renderer) => renderer.kind)).toEqual([
12
+ "image",
13
+ "code-block",
14
+ "inline-code",
15
+ "heading",
16
+ "paragraph",
17
+ "emphasis",
18
+ "blockquote",
19
+ "link",
20
+ "list",
21
+ "table",
22
+ "divider",
23
+ "hard-break",
24
+ "article",
25
+ ]);
26
+ });
27
+
28
+ test("allows a built-in renderer to be replaced without changing the registry", () => {
29
+ const paragraphOverride: WechatElementRenderer = {
30
+ kind: "paragraph",
31
+ selector: "p[data-custom]",
32
+ };
33
+ const registry = createWechatRendererRegistry({ paragraph: paragraphOverride });
34
+
35
+ expect(registry.find((renderer) => renderer.kind === "paragraph")).toBe(paragraphOverride);
36
+ expect(registry).toHaveLength(13);
37
+ });
38
+
39
+ test("emits compatibility selectors and account theme tokens", () => {
40
+ const theme = getWechatPreviewTheme("default");
41
+ const css = buildWechatThemeCss(theme.editorVars, theme.exportTheme);
42
+
43
+ expect(css).toContain(".milkdown .editor");
44
+ expect(css).toContain("--wx-body-line-height: 1.84");
45
+ expect(css).toContain("--wx-h2-color: #1f1b1c");
46
+ expect(css).toContain("--wx-h3-color: #5c5658");
47
+ expect(css).toContain("--wx-brand-ink: #a94473");
48
+ expect(css).toContain('[data-wechat-node="footer"]');
49
+ });
50
+ });