@zzclub/pipeline 0.2.0 → 0.4.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 (194) hide show
  1. package/README.md +366 -170
  2. package/package.json +4 -7
  3. package/src/adapter-loader.ts +138 -0
  4. package/src/adapter-types.ts +172 -0
  5. package/src/adapters/builtin-image-renderer.ts +353 -0
  6. package/src/adapters/builtin-markdown-renderer.ts +63 -0
  7. package/src/adapters/index.ts +2 -0
  8. package/src/args.test.ts +118 -0
  9. package/src/args.ts +90 -0
  10. package/src/cli.ts +51 -0
  11. package/src/commands/abandon.test.ts +104 -0
  12. package/src/commands/abandon.ts +202 -0
  13. package/src/commands/attach-body-images.ts +88 -0
  14. package/src/commands/attach-body.ts +86 -0
  15. package/src/commands/attach-newspic-spec.ts +41 -0
  16. package/src/commands/checkpoint.test.ts +91 -0
  17. package/src/commands/checkpoint.ts +66 -0
  18. package/src/commands/config.ts +106 -0
  19. package/src/commands/cos-upload.ts +62 -0
  20. package/src/commands/doctor.ts +66 -0
  21. package/src/commands/find-run.ts +55 -0
  22. package/src/commands/hermes-metrics.ts +226 -0
  23. package/src/commands/imgx.ts +60 -0
  24. package/src/commands/ingest-handoff.ts +509 -0
  25. package/src/commands/init.ts +126 -0
  26. package/src/commands/prepare-finalize.ts +348 -0
  27. package/src/commands/prepare.ts +310 -0
  28. package/src/commands/publish.ts +147 -0
  29. package/src/commands/reconcile.ts +36 -0
  30. package/src/commands/render.ts +244 -0
  31. package/src/commands/reset.test.ts +151 -0
  32. package/src/commands/reset.ts +159 -0
  33. package/src/commands/review.ts +77 -0
  34. package/src/commands/status.ts +68 -0
  35. package/src/commands/sync-blog.ts +39 -0
  36. package/src/commands/tasks.ts +50 -0
  37. package/src/commands/wechat-export.ts +68 -0
  38. package/src/config.ts +0 -0
  39. package/src/imgx/ascii-portrait.ts +77 -0
  40. package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
  41. package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
  42. package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  43. package/src/imgx/assets/icons/logo.png +0 -0
  44. package/src/imgx/assets/icons/logo.svg +8 -0
  45. package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
  46. package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
  47. package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
  48. package/src/imgx/cli.ts +57 -0
  49. package/src/imgx/geometry.ts +68 -0
  50. package/src/imgx/index.ts +5 -0
  51. package/src/imgx/longform-theme.ts +243 -0
  52. package/src/imgx/poster-recipe.ts +81 -0
  53. package/src/imgx/pretext-adapter.ts +14 -0
  54. package/src/imgx/pretext-runtime.ts +61 -0
  55. package/src/imgx/references/ascii-portrait-3-4.md +59 -0
  56. package/src/imgx/references/longform-3-4.md +173 -0
  57. package/src/imgx/references/poster-3-4.md +87 -0
  58. package/src/imgx/references/tips-3-4.md +69 -0
  59. package/src/imgx/references/wechat-cover-split.md +89 -0
  60. package/src/imgx/references/x-like-posts.md +139 -0
  61. package/src/imgx/render-article.test.ts +53 -0
  62. package/src/imgx/render-article.ts +1364 -0
  63. package/src/imgx/render-ascii-portrait.ts +53 -0
  64. package/src/imgx/render-card.ts +232 -0
  65. package/src/imgx/render-x-like-posts.ts +235 -0
  66. package/src/imgx/runtime.ts +403 -0
  67. package/src/imgx/typographic-scale.test.ts +56 -0
  68. package/src/imgx/typographic-scale.ts +44 -0
  69. package/src/output.ts +632 -0
  70. package/src/plugins.ts +101 -0
  71. package/src/profiles.ts +149 -0
  72. package/src/providers/blog.test.ts +58 -0
  73. package/src/providers/blog.ts +154 -0
  74. package/src/providers/cos.ts +183 -0
  75. package/src/providers/index.test.ts +26 -0
  76. package/src/providers/index.ts +185 -0
  77. package/src/providers/wechat.test.ts +215 -0
  78. package/src/providers/wechat.ts +548 -0
  79. package/src/routes.ts +287 -0
  80. package/src/runtime-paths.ts +179 -0
  81. package/src/schema/config.ts +133 -0
  82. package/src/schema/state.ts +376 -0
  83. package/src/spawn.ts +103 -0
  84. package/src/state.ts +265 -0
  85. package/src/task-manager.ts +657 -0
  86. package/src/task-views.ts +222 -0
  87. package/src/text.test.ts +689 -0
  88. package/src/text.ts +683 -0
  89. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  90. package/src/wechat-preview/browser/editor-export.css +241 -0
  91. package/src/wechat-preview/browser/editor-export.ts +103 -0
  92. package/src/wechat-preview/frontmatter-handler.ts +73 -0
  93. package/src/wechat-preview/index.test.ts +154 -0
  94. package/src/wechat-preview/index.ts +294 -0
  95. package/src/wechat-preview/themes.ts +141 -0
  96. package/src/wechat-preview/wechat-formatter.test.ts +288 -0
  97. package/src/wechat-preview/wechat-formatter.ts +1506 -0
  98. package/src/wechat-preview.test.ts +73 -0
  99. package/src/workflow-materials.test.ts +225 -0
  100. package/src/workflow-materials.ts +217 -0
  101. package/src/workflow.test.ts +2087 -0
  102. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  103. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  104. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  105. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  106. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
  107. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  108. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
  109. package/dist/cli.js +0 -432
  110. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
  111. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
  112. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
  113. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
  114. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
  115. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
  116. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
  117. package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
  118. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
  119. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
  120. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
  121. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
  122. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
  123. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
  124. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
  125. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
  126. /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
  127. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
  128. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
  129. /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
  130. /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
  131. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
  132. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  133. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  134. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  135. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  136. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  137. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  138. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  139. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  140. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  141. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  142. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  143. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  144. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  145. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  146. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  147. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  148. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  149. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  150. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  151. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  152. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  153. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  154. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  155. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  156. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  157. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  158. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  159. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  160. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  161. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  162. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  163. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  164. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  165. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  166. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  167. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  168. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  169. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  170. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  171. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  172. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  173. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  174. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  175. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  176. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  177. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  178. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  179. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  180. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  181. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  182. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  183. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  184. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  185. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  186. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  187. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  188. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  189. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  190. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  191. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  192. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
  193. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
  194. /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
@@ -0,0 +1,294 @@
1
+ import { existsSync } from "fs";
2
+ import { cp, mkdir, readFile, rm, writeFile } from "fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import {
6
+ dumpHtmlDom,
7
+ ensureParentDir,
8
+ escapeHtml,
9
+ findChrome,
10
+ readUtf8,
11
+ renderTemplate,
12
+ } from "../imgx/runtime";
13
+ import {
14
+ PACKAGE_ROOT,
15
+ TEMPLATE_PATH,
16
+ DIST_DIR,
17
+ MANIFEST_PATH,
18
+ VITE_CONFIG_PATH,
19
+ } from "../runtime-paths";
20
+ import { extractFrontmatter } from "./frontmatter-handler";
21
+ import { getWechatPreviewStyleName, getWechatPreviewTheme } from "./themes";
22
+ import { stripLeadingH1 } from "../text";
23
+
24
+ interface ViteManifestEntry {
25
+ file: string;
26
+ css?: string[];
27
+ }
28
+
29
+ interface BundleAssets {
30
+ scriptPath: string;
31
+ cssPath: string | null;
32
+ }
33
+
34
+ export interface ExportMarkdownToWechatHtmlInput {
35
+ markdownPath: string;
36
+ outPath: string;
37
+ account: string;
38
+ title?: string;
39
+ previewShellOutPath?: string;
40
+ customCss?: string | null;
41
+ }
42
+
43
+ export interface ExportMarkdownToWechatHtmlResult {
44
+ html: string;
45
+ htmlPath: string;
46
+ account: string;
47
+ previewStyle: string;
48
+ previewShellPath?: string;
49
+ }
50
+
51
+ function ensureBundle(): BundleAssets {
52
+ if (!existsSync(MANIFEST_PATH)) {
53
+ const result = Bun.spawnSync({
54
+ cmd: ["bun", "x", "vite", "build", "--config", VITE_CONFIG_PATH],
55
+ cwd: PACKAGE_ROOT,
56
+ stdout: "pipe",
57
+ stderr: "pipe",
58
+ });
59
+ if (result.exitCode !== 0) {
60
+ const stderr = new TextDecoder().decode(result.stderr);
61
+ throw new Error(`Failed to build wechat preview bundle:\n${stderr}`);
62
+ }
63
+ }
64
+
65
+ const manifestRaw = readUtf8(MANIFEST_PATH);
66
+ const manifest = JSON.parse(manifestRaw) as Record<string, ViteManifestEntry>;
67
+ const entry =
68
+ manifest["src/wechat-preview/browser/editor-export.ts"] ??
69
+ Object.values(manifest)[0];
70
+
71
+ if (!entry?.file) {
72
+ throw new Error("Wechat preview manifest missing editor-export entry");
73
+ }
74
+
75
+ return {
76
+ scriptPath: join(DIST_DIR, entry.file),
77
+ cssPath: entry.css?.[0]
78
+ ? join(DIST_DIR, entry.css[0])
79
+ : manifest["style.css"]?.file
80
+ ? join(DIST_DIR, manifest["style.css"].file)
81
+ : null,
82
+ };
83
+ }
84
+
85
+ export function escapeInlineJson(value: string): string {
86
+ return value
87
+ .replace(/</g, "\\u003C")
88
+ .replace(/>/g, "\\u003E")
89
+ .replace(/&/g, "\\u0026");
90
+ }
91
+
92
+ export function isExternalUrl(value: string): boolean {
93
+ return /^(https?:|data:|blob:|file:|\/\/)/i.test(value);
94
+ }
95
+
96
+ export function resolveMarkdownAsset(rawPath: string, baseDir: string): string {
97
+ const trimmed = rawPath.trim();
98
+ if (!trimmed || isExternalUrl(trimmed) || isAbsolute(trimmed)) {
99
+ return trimmed;
100
+ }
101
+ return resolve(baseDir, trimmed);
102
+ }
103
+
104
+ export function rewriteRelativeImagePaths(markdown: string, baseDir: string): string {
105
+ return markdown
106
+ .replace(/!\[([^\]]*)\]\((<)?([^)]+?)(>)?\)/g, (_match, alt, open, src) => {
107
+ const resolved = resolveMarkdownAsset(String(src ?? ""), baseDir);
108
+ if (!resolved || resolved === src) {
109
+ return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
110
+ }
111
+ return `![${alt}](<${resolved}>)`;
112
+ })
113
+ .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
114
+ const resolved = resolveMarkdownAsset(String(src ?? ""), baseDir);
115
+ return `${prefix}${resolved}${suffix}`;
116
+ });
117
+ }
118
+
119
+ function rewriteRelativeImagePathsForPreview(markdown: string, baseDir: string, previewDir: string): string {
120
+ return markdown
121
+ .replace(/!\[([^\]]*)\]\((<)?([^)]+?)(>)?\)/g, (_match, alt, open, src) => {
122
+ const raw = String(src ?? "").trim();
123
+ if (!raw || isExternalUrl(raw)) {
124
+ return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
125
+ }
126
+ const resolved = resolveMarkdownAsset(raw, baseDir);
127
+ if (!resolved) {
128
+ return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
129
+ }
130
+ const previewRelative = relative(previewDir, resolved).replaceAll("\\", "/");
131
+ const normalized = previewRelative.startsWith(".") ? previewRelative : `./${previewRelative}`;
132
+ return `![${alt}](${open ? "<" : ""}${normalized}${open ? ">" : ""})`;
133
+ })
134
+ .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
135
+ const raw = String(src ?? "").trim();
136
+ if (!raw || isExternalUrl(raw)) {
137
+ return `${prefix}${src}${suffix}`;
138
+ }
139
+ const resolved = resolveMarkdownAsset(raw, baseDir);
140
+ const previewRelative = relative(previewDir, resolved).replaceAll("\\", "/");
141
+ const normalized = previewRelative.startsWith(".") ? previewRelative : `./${previewRelative}`;
142
+ return `${prefix}${normalized}${suffix}`;
143
+ });
144
+ }
145
+
146
+ async function ensurePreviewBundleDir(targetDir: string): Promise<void> {
147
+ await mkdir(dirname(targetDir), { recursive: true });
148
+ await rm(targetDir, { recursive: true, force: true });
149
+ await cp(DIST_DIR, targetDir, { recursive: true, force: true });
150
+ }
151
+
152
+ async function writePreviewShell(options: {
153
+ previewShellOutPath: string;
154
+ pageTitle: string;
155
+ sourceMarkdown: string;
156
+ sourceMarkdownDir: string;
157
+ theme: ReturnType<typeof getWechatPreviewTheme>;
158
+ bundle: BundleAssets;
159
+ }): Promise<string> {
160
+ const previewDir = dirname(options.previewShellOutPath);
161
+ await mkdir(previewDir, { recursive: true });
162
+ const previewBundleDir = join(previewDir, "browser-dist");
163
+ await ensurePreviewBundleDir(previewBundleDir);
164
+
165
+ const previewMarkdown = rewriteRelativeImagePathsForPreview(
166
+ options.sourceMarkdown,
167
+ options.sourceMarkdownDir,
168
+ previewDir,
169
+ );
170
+ const shell = readUtf8(TEMPLATE_PATH);
171
+ const payloadJson = escapeInlineJson(
172
+ JSON.stringify({
173
+ markdown: previewMarkdown,
174
+ editorVars: options.theme.editorVars,
175
+ exportTheme: options.theme.exportTheme,
176
+ }),
177
+ );
178
+ const relativeScriptPath = relative(previewDir, join(previewBundleDir, "editor-export.js")).replaceAll("\\", "/");
179
+ const relativeCssPath = options.bundle.cssPath
180
+ ? relative(previewDir, join(previewBundleDir, relative(DIST_DIR, options.bundle.cssPath))).replaceAll("\\", "/")
181
+ : null;
182
+
183
+ const shellHtml = renderTemplate(shell, {
184
+ "{{PAGE_TITLE}}": escapeHtml(options.pageTitle),
185
+ "{{CSS_LINK}}": relativeCssPath ? `<link rel="stylesheet" href="${relativeCssPath.startsWith(".") ? relativeCssPath : `./${relativeCssPath}`}">` : "",
186
+ "{{PAYLOAD_JSON}}": payloadJson,
187
+ "{{SCRIPT_URL}}": relativeScriptPath.startsWith(".") ? relativeScriptPath : `./${relativeScriptPath}`,
188
+ });
189
+
190
+ await writeFile(options.previewShellOutPath, shellHtml, "utf-8");
191
+ return options.previewShellOutPath;
192
+ }
193
+
194
+ export function extractRenderResult(
195
+ dom: string,
196
+ ): { status: "success"; html: string } | { status: "error"; error: string } | { status: string } {
197
+ const match = dom.match(
198
+ /<script id="zzhub-wechat-export-result" type="application\/json">([\s\S]*?)<\/script>/,
199
+ );
200
+
201
+ if (!match?.[1]) {
202
+ throw new Error("Wechat preview result script not found");
203
+ }
204
+
205
+ return JSON.parse(match[1]) as
206
+ | { status: "success"; html: string }
207
+ | { status: "error"; error: string }
208
+ | { status: string };
209
+ }
210
+
211
+ export async function exportMarkdownToWechatHtml(
212
+ input: ExportMarkdownToWechatHtmlInput,
213
+ ): Promise<ExportMarkdownToWechatHtmlResult> {
214
+ const chromePath = findChrome();
215
+ if (chromePath === null) {
216
+ throw new Error(
217
+ "Chrome/Chromium not found. Required for WeChat HTML export.\n" +
218
+ "Install one of:\n" +
219
+ " macOS: brew install --cask chromium\n" +
220
+ " Ubuntu: sudo apt install chromium-browser\n" +
221
+ " Or install Google Chrome from https://www.google.com/chrome/\n" +
222
+ " Or set CHROME_PATH environment variable to the binary path.",
223
+ );
224
+ }
225
+
226
+ const bundle = ensureBundle();
227
+ const sourceMarkdown = await readFile(input.markdownPath, "utf-8");
228
+ const stripped = extractFrontmatter(sourceMarkdown).content;
229
+ const bodyMarkdown = stripLeadingH1(stripped);
230
+ const markdown = rewriteRelativeImagePaths(bodyMarkdown, dirname(input.markdownPath));
231
+ const theme = getWechatPreviewTheme(input.account);
232
+ const shell = readUtf8(TEMPLATE_PATH);
233
+ const payloadJson = escapeInlineJson(
234
+ JSON.stringify({
235
+ markdown,
236
+ editorVars: theme.editorVars,
237
+ exportTheme: theme.exportTheme,
238
+ }),
239
+ );
240
+
241
+ let customCssTag = "";
242
+ if (input.customCss) {
243
+ const cssContent = await readFile(input.customCss, "utf-8");
244
+ customCssTag = `<style>${cssContent}</style>`;
245
+ }
246
+
247
+ const shellHtml = renderTemplate(shell, {
248
+ "{{PAGE_TITLE}}": escapeHtml(input.title ?? "Wechat Preview Export"),
249
+ "{{CSS_LINK}}": [
250
+ bundle.cssPath
251
+ ? `<link rel="stylesheet" href="${pathToFileURL(bundle.cssPath).href}">`
252
+ : "",
253
+ customCssTag,
254
+ ].filter(Boolean).join("\n "),
255
+ "{{PAYLOAD_JSON}}": payloadJson,
256
+ "{{SCRIPT_URL}}": pathToFileURL(bundle.scriptPath).href,
257
+ });
258
+
259
+ const dom = dumpHtmlDom({
260
+ chromePath,
261
+ html: shellHtml,
262
+ virtualTimeBudgetMs: 15000,
263
+ });
264
+ const result = extractRenderResult(dom);
265
+ if (result.status === "error" && "error" in result) {
266
+ throw new Error(`Wechat preview export failed: ${result.error}`);
267
+ }
268
+ if (result.status !== "success" || !("html" in result)) {
269
+ throw new Error(`Wechat preview export did not finish before timeout (status=${result.status})`);
270
+ }
271
+
272
+ ensureParentDir(input.outPath);
273
+ await writeFile(input.outPath, result.html, "utf-8");
274
+
275
+ let previewShellPath: string | undefined;
276
+ if (input.previewShellOutPath) {
277
+ previewShellPath = await writePreviewShell({
278
+ previewShellOutPath: input.previewShellOutPath,
279
+ pageTitle: input.title ?? "Wechat Preview Export",
280
+ sourceMarkdown: bodyMarkdown,
281
+ sourceMarkdownDir: dirname(input.markdownPath),
282
+ theme,
283
+ bundle,
284
+ });
285
+ }
286
+
287
+ return {
288
+ html: result.html,
289
+ htmlPath: input.outPath,
290
+ account: input.account,
291
+ previewStyle: getWechatPreviewStyleName(input.account),
292
+ previewShellPath,
293
+ };
294
+ }
@@ -0,0 +1,141 @@
1
+ export interface WechatExportTheme {
2
+ containerStyle: string;
3
+ footerText: string;
4
+ footerStyle: string;
5
+ fontFamily: string;
6
+ bodyColor: string;
7
+ mutedColor: string;
8
+ primaryColor: string;
9
+ dividerColor: string;
10
+ blockquoteBorderColor: string;
11
+ bodyLineHeight: string;
12
+ bodyLetterSpacing: string;
13
+ }
14
+
15
+ export interface WechatPreviewTheme {
16
+ name: string;
17
+ account: string;
18
+ editorVars: Record<string, string>;
19
+ exportTheme: WechatExportTheme;
20
+ }
21
+
22
+ const BASE_EDITOR_VARS: Record<string, string> = {
23
+ "--background": "0 0% 100%",
24
+ "--foreground": "240 10% 3.9%",
25
+ "--card": "0 0% 100%",
26
+ "--card-foreground": "240 10% 3.9%",
27
+ "--muted": "240 4.8% 95.9%",
28
+ "--muted-foreground": "240 3.8% 46.1%",
29
+ "--primary-foreground": "0 0% 100%",
30
+ "--border": "240 5.9% 90%",
31
+ };
32
+
33
+ function buildContainerStyle(options: {
34
+ background: string;
35
+ color: string;
36
+ fontFamily: string;
37
+ lineHeight?: string;
38
+ letterSpacing?: string;
39
+ }): string {
40
+ return [
41
+ "padding: 14px 11px",
42
+ `background: ${options.background}`,
43
+ "background-size: 20px 20px",
44
+ "background-position: center center",
45
+ `font-family: ${options.fontFamily}`,
46
+ "font-size: 16px",
47
+ `color: ${options.color}`,
48
+ "word-wrap: break-word",
49
+ `line-height: ${options.lineHeight ?? "1.92"}`,
50
+ `letter-spacing: ${options.letterSpacing ?? "0.03em"}`,
51
+ ].join("; ");
52
+ }
53
+
54
+ function buildFooterStyle(color: string): string {
55
+ return [
56
+ "margin-top: 32px",
57
+ "text-align: center",
58
+ "font-size: 12px",
59
+ `color: ${color}`,
60
+ ].join("; ");
61
+ }
62
+
63
+ const THEMES: Record<string, WechatPreviewTheme> = {
64
+ default: {
65
+ name: "sage-journal",
66
+ account: "default",
67
+ editorVars: {
68
+ ...BASE_EDITOR_VARS,
69
+ "--primary": "220 9% 10%",
70
+ "--ring": "220 9% 10%",
71
+ "--accent": "210 14% 96%",
72
+ "--accent-foreground": "220 9% 10%",
73
+ "--muted": "210 14% 96%",
74
+ "--muted-foreground": "215 8% 42%",
75
+ "--border": "220 10% 86%",
76
+ },
77
+ exportTheme: {
78
+ containerStyle: buildContainerStyle({
79
+ background: "#ffffff",
80
+ color: "#202124",
81
+ fontFamily:
82
+ "'SweiCurveLeg', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
83
+ lineHeight: "1.92",
84
+ letterSpacing: "0.03em",
85
+ }),
86
+ footerText: "公众号 · 早早集市",
87
+ footerStyle: buildFooterStyle("#8a8f98"),
88
+ fontFamily:
89
+ "'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
90
+ bodyColor: "#202124",
91
+ mutedColor: "#5f6368",
92
+ primaryColor: "#111111",
93
+ dividerColor: "#dadce0",
94
+ blockquoteBorderColor: "#3c4043",
95
+ bodyLineHeight: "1.92",
96
+ bodyLetterSpacing: "0.03em",
97
+ },
98
+ },
99
+ ancientone: {
100
+ name: "rose-ledger",
101
+ account: "ancientone",
102
+ editorVars: {
103
+ ...BASE_EDITOR_VARS,
104
+ "--primary": "330 50% 58%",
105
+ "--ring": "330 50% 58%",
106
+ "--accent": "330 35% 96%",
107
+ "--accent-foreground": "334 28% 22%",
108
+ "--muted": "336 38% 97%",
109
+ "--border": "334 26% 88%",
110
+ },
111
+ exportTheme: {
112
+ containerStyle: buildContainerStyle({
113
+ background: "#fffafb",
114
+ color: "#4a3a44",
115
+ fontFamily:
116
+ "'SweiCurveLeg', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
117
+ lineHeight: "1.94",
118
+ letterSpacing: "0.035em",
119
+ }),
120
+ footerText: "公众号 · 古一软件",
121
+ footerStyle: buildFooterStyle("#b08a9d"),
122
+ fontFamily:
123
+ "'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
124
+ bodyColor: "#4a3a44",
125
+ mutedColor: "#7a6271",
126
+ primaryColor: "#b35d85",
127
+ dividerColor: "#ead5df",
128
+ blockquoteBorderColor: "#d6a8bc",
129
+ bodyLineHeight: "1.94",
130
+ bodyLetterSpacing: "0.035em",
131
+ },
132
+ },
133
+ };
134
+
135
+ export function getWechatPreviewTheme(account: string): WechatPreviewTheme {
136
+ return THEMES[account] ?? THEMES.default;
137
+ }
138
+
139
+ export function getWechatPreviewStyleName(account: string): string {
140
+ return getWechatPreviewTheme(account).name;
141
+ }