@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.
- package/README.md +113 -8
- package/package.json +4 -4
- package/src/adapter-types.ts +21 -0
- package/src/adapters/builtin-markdown-renderer.ts +7 -0
- package/src/cli.ts +5 -1
- package/src/commands/ingest-handoff.ts +1 -21
- package/src/commands/prepare-finalize.ts +2 -1
- package/src/commands/prepare.ts +30 -19
- package/src/commands/publish.ts +2 -3
- package/src/commands/reconcile.ts +3 -5
- package/src/commands/render.ts +2 -1
- package/src/commands/republish.ts +2 -3
- package/src/commands/reset.ts +9 -1
- package/src/commands/review.ts +3 -2
- package/src/commands/sync-blog.ts +2 -1
- package/src/commands/wechat-export.test.ts +41 -0
- package/src/commands/wechat-export.ts +156 -20
- package/src/commands/wechat-preview.ts +160 -0
- package/src/config.ts +12 -0
- package/src/imgx/runtime.ts +31 -3
- package/src/logger.test.ts +56 -0
- package/src/logger.ts +178 -0
- package/src/plugins.ts +2 -0
- package/src/providers/blog.ts +1 -2
- package/src/providers/index.ts +35 -3
- package/src/publish-targets.ts +4 -5
- package/src/routes.ts +9 -1
- package/src/schema/config.ts +2 -0
- package/src/state.ts +24 -0
- package/src/task-manager.test.ts +57 -0
- package/src/task-manager.ts +23 -5
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1 -302
- package/src/wechat-preview/assets/browser-dist/editor-export.js +500 -958
- package/src/wechat-preview/browser/assets.d.ts +9 -0
- package/src/wechat-preview/browser/editor-export.css +5 -326
- package/src/wechat-preview/browser/editor-export.ts +30 -65
- package/src/wechat-preview/bundle-stale.test.ts +52 -0
- package/src/wechat-preview/fixtures/all-nodes.md +68 -0
- package/src/wechat-preview/index.test.ts +17 -0
- package/src/wechat-preview/index.ts +424 -180
- package/src/wechat-preview/server/client.ts +164 -0
- package/src/wechat-preview/server/http.ts +229 -0
- package/src/wechat-preview/server/index.ts +33 -0
- package/src/wechat-preview/server/local-file.test.ts +52 -0
- package/src/wechat-preview/server/local-file.ts +116 -0
- package/src/wechat-preview/server/paths.ts +80 -0
- package/src/wechat-preview/server/registry.test.ts +60 -0
- package/src/wechat-preview/server/registry.ts +118 -0
- package/src/wechat-preview/server/singleton.ts +118 -0
- package/src/wechat-preview/server/types.ts +61 -0
- package/src/wechat-preview/server/ui.ts +324 -0
- package/src/wechat-preview/themes.ts +58 -45
- package/src/wechat-preview/wechat-export.integration.test.ts +94 -0
- package/src/wechat-preview/wechat-renderer.links.test.ts +44 -0
- package/src/wechat-preview/wechat-renderer.test.ts +50 -0
- package/src/wechat-preview/wechat-renderer.ts +698 -0
- package/src/wechat-preview.test.ts +25 -41
- package/src/workflow-materials.test.ts +52 -1
- package/src/workflow-materials.ts +12 -0
- package/src/workflow.test.ts +105 -5
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/style-vhHYSP8T.css +0 -1
- package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
- package/src/wechat-preview/wechat-formatter.test.ts +0 -287
- package/src/wechat-preview/wechat-formatter.ts +0 -1531
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, statSync } from "fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
4
5
|
import { pathToFileURL } from "url";
|
|
5
6
|
import {
|
|
7
|
+
ChromeDumpError,
|
|
6
8
|
dumpHtmlDom,
|
|
7
9
|
ensureParentDir,
|
|
8
10
|
escapeHtml,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
DIST_DIR,
|
|
17
19
|
MANIFEST_PATH,
|
|
18
20
|
VITE_CONFIG_PATH,
|
|
21
|
+
WECHAT_PREVIEW_DIR,
|
|
19
22
|
} from "../runtime-paths";
|
|
20
23
|
import { extractFrontmatter } from "./frontmatter-handler";
|
|
21
24
|
import { getWechatPreviewStyleName, getWechatPreviewTheme } from "./themes";
|
|
@@ -23,12 +26,61 @@ import { stripLeadingH1 } from "../text";
|
|
|
23
26
|
|
|
24
27
|
interface ViteManifestEntry {
|
|
25
28
|
file: string;
|
|
26
|
-
css?: string[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
interface BundleAssets {
|
|
30
32
|
scriptPath: string;
|
|
31
|
-
|
|
33
|
+
rebuilt: boolean;
|
|
34
|
+
stale: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type WechatExportErrorKind =
|
|
38
|
+
| "chrome_missing"
|
|
39
|
+
| "chrome_failed"
|
|
40
|
+
| "timeout"
|
|
41
|
+
| "render_error"
|
|
42
|
+
| "bundle"
|
|
43
|
+
| "other";
|
|
44
|
+
|
|
45
|
+
export interface WechatExportDebugInfo {
|
|
46
|
+
chrome_path?: string;
|
|
47
|
+
virtual_time_budget_ms?: number;
|
|
48
|
+
bundle_stale?: boolean;
|
|
49
|
+
bundle_rebuilt?: boolean;
|
|
50
|
+
shell_path?: string;
|
|
51
|
+
stderr_tail?: string;
|
|
52
|
+
debug_dir?: string;
|
|
53
|
+
semantic_html?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class WechatExportError extends Error {
|
|
57
|
+
readonly kind: WechatExportErrorKind;
|
|
58
|
+
readonly durationMs: number;
|
|
59
|
+
readonly debug: WechatExportDebugInfo;
|
|
60
|
+
readonly account: string;
|
|
61
|
+
readonly title?: string;
|
|
62
|
+
readonly markdownPath?: string;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
message: string,
|
|
66
|
+
options: {
|
|
67
|
+
kind: WechatExportErrorKind;
|
|
68
|
+
durationMs: number;
|
|
69
|
+
debug?: WechatExportDebugInfo;
|
|
70
|
+
account: string;
|
|
71
|
+
title?: string;
|
|
72
|
+
markdownPath?: string;
|
|
73
|
+
},
|
|
74
|
+
) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "WechatExportError";
|
|
77
|
+
this.kind = options.kind;
|
|
78
|
+
this.durationMs = options.durationMs;
|
|
79
|
+
this.debug = options.debug ?? {};
|
|
80
|
+
this.account = options.account;
|
|
81
|
+
this.title = options.title;
|
|
82
|
+
this.markdownPath = options.markdownPath;
|
|
83
|
+
}
|
|
32
84
|
}
|
|
33
85
|
|
|
34
86
|
export interface ExportMarkdownToWechatHtmlInput {
|
|
@@ -42,6 +94,10 @@ export interface ExportMarkdownToWechatHtmlInput {
|
|
|
42
94
|
editorVars?: Record<string, string>;
|
|
43
95
|
exportTheme?: Record<string, string>;
|
|
44
96
|
};
|
|
97
|
+
/** Chrome virtual-time-budget in ms (default 15000). */
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
/** Write intermediate artifacts for debugging. */
|
|
100
|
+
debugDir?: string;
|
|
45
101
|
}
|
|
46
102
|
|
|
47
103
|
export interface ExportMarkdownToWechatHtmlResult {
|
|
@@ -50,22 +106,71 @@ export interface ExportMarkdownToWechatHtmlResult {
|
|
|
50
106
|
account: string;
|
|
51
107
|
previewStyle: string;
|
|
52
108
|
previewShellPath?: string;
|
|
109
|
+
durationMs: number;
|
|
110
|
+
bundleRebuilt: boolean;
|
|
111
|
+
bundleStale: boolean;
|
|
112
|
+
debugDir?: string;
|
|
113
|
+
semanticHtml?: string;
|
|
53
114
|
}
|
|
54
115
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
117
|
+
|
|
118
|
+
/** Absolute path to shared article.css (@zzclub/milkdown-article-style). */
|
|
119
|
+
export function resolveMilkdownArticleStylePath(): string {
|
|
120
|
+
try {
|
|
121
|
+
return requireFromHere.resolve("@zzclub/milkdown-article-style/article.css");
|
|
122
|
+
} catch {
|
|
123
|
+
// Bun / Node may need package root + relative when exports are CSS-only.
|
|
124
|
+
const pkgRoot = dirname(requireFromHere.resolve("@zzclub/milkdown-article-style/package.json"));
|
|
125
|
+
return join(pkgRoot, "article.css");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Source files that feed the browser Vite bundle. */
|
|
130
|
+
export function getWechatPreviewBundleSources(): string[] {
|
|
131
|
+
return [
|
|
132
|
+
join(WECHAT_PREVIEW_DIR, "browser/editor-export.ts"),
|
|
133
|
+
resolveMilkdownArticleStylePath(),
|
|
134
|
+
join(WECHAT_PREVIEW_DIR, "wechat-renderer.ts"),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isWechatPreviewBundleStale(
|
|
139
|
+
sources: string[] = getWechatPreviewBundleSources(),
|
|
140
|
+
manifestPath: string = MANIFEST_PATH,
|
|
141
|
+
): boolean {
|
|
142
|
+
if (!existsSync(manifestPath)) return true;
|
|
143
|
+
let distMtime: number;
|
|
144
|
+
try {
|
|
145
|
+
distMtime = statSync(manifestPath).mtimeMs;
|
|
146
|
+
} catch {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
for (const src of sources) {
|
|
150
|
+
if (!existsSync(src)) continue;
|
|
151
|
+
try {
|
|
152
|
+
if (statSync(src).mtimeMs > distMtime) return true;
|
|
153
|
+
} catch {
|
|
154
|
+
// ignore unreadable sources
|
|
66
155
|
}
|
|
67
156
|
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildBundle(): void {
|
|
161
|
+
const result = Bun.spawnSync({
|
|
162
|
+
cmd: ["bun", "x", "vite", "build", "--config", VITE_CONFIG_PATH],
|
|
163
|
+
cwd: PACKAGE_ROOT,
|
|
164
|
+
stdout: "pipe",
|
|
165
|
+
stderr: "pipe",
|
|
166
|
+
});
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
const stderr = new TextDecoder().decode(result.stderr);
|
|
169
|
+
throw new Error(`Failed to build wechat preview bundle:\n${stderr}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
68
172
|
|
|
173
|
+
function readBundleScriptPath(): string {
|
|
69
174
|
const manifestRaw = readUtf8(MANIFEST_PATH);
|
|
70
175
|
const manifest = JSON.parse(manifestRaw) as Record<string, ViteManifestEntry>;
|
|
71
176
|
const entry =
|
|
@@ -76,13 +181,28 @@ function ensureBundle(): BundleAssets {
|
|
|
76
181
|
throw new Error("Wechat preview manifest missing editor-export entry");
|
|
77
182
|
}
|
|
78
183
|
|
|
184
|
+
return join(DIST_DIR, entry.file);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensureBundle(): BundleAssets {
|
|
188
|
+
const stale = isWechatPreviewBundleStale();
|
|
189
|
+
let rebuilt = false;
|
|
190
|
+
if (!existsSync(MANIFEST_PATH) || stale) {
|
|
191
|
+
try {
|
|
192
|
+
buildBundle();
|
|
193
|
+
rebuilt = true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Fall back to existing dist when rebuild tooling is unavailable (e.g. missing juice).
|
|
196
|
+
if (!existsSync(MANIFEST_PATH)) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
79
202
|
return {
|
|
80
|
-
scriptPath:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: manifest["style.css"]?.file
|
|
84
|
-
? join(DIST_DIR, manifest["style.css"].file)
|
|
85
|
-
: null,
|
|
203
|
+
scriptPath: readBundleScriptPath(),
|
|
204
|
+
rebuilt,
|
|
205
|
+
stale: stale && !rebuilt,
|
|
86
206
|
};
|
|
87
207
|
}
|
|
88
208
|
|
|
@@ -94,7 +214,7 @@ export function escapeInlineJson(value: string): string {
|
|
|
94
214
|
}
|
|
95
215
|
|
|
96
216
|
export function isExternalUrl(value: string): boolean {
|
|
97
|
-
return /^(
|
|
217
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value);
|
|
98
218
|
}
|
|
99
219
|
|
|
100
220
|
export function resolveMarkdownAsset(rawPath: string, baseDir: string): string {
|
|
@@ -125,92 +245,49 @@ export function rewriteRelativeImagePaths(markdown: string, baseDir: string): st
|
|
|
125
245
|
});
|
|
126
246
|
}
|
|
127
247
|
|
|
128
|
-
function
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const titleStr = title ? ` "${title}"` : "";
|
|
143
|
-
return ``;
|
|
144
|
-
}
|
|
145
|
-
const previewRelative = relative(previewDir, resolved).replaceAll("\\", "/");
|
|
146
|
-
const normalized = previewRelative.startsWith(".") ? previewRelative : `./${previewRelative}`;
|
|
147
|
-
const wrap = !!(hasAngleBrackets || normalized.includes(" "));
|
|
148
|
-
const titleStr = title ? ` "${title}"` : "";
|
|
149
|
-
return ``;
|
|
150
|
-
})
|
|
151
|
-
.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
|
|
152
|
-
const raw = String(src ?? "").trim();
|
|
153
|
-
if (!raw || isExternalUrl(raw)) {
|
|
154
|
-
return `${prefix}${src}${suffix}`;
|
|
248
|
+
export function buildWechatPreviewShell(articleHtml: string, pageTitle: string): string {
|
|
249
|
+
return `<!doctype html>
|
|
250
|
+
<html lang="zh-CN">
|
|
251
|
+
<head>
|
|
252
|
+
<meta charset="utf-8">
|
|
253
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
254
|
+
<title>${escapeHtml(pageTitle)}</title>
|
|
255
|
+
<style>
|
|
256
|
+
html { background: #f3f1f2; }
|
|
257
|
+
body { margin: 0; padding: 32px 16px 64px; background: #f3f1f2; }
|
|
258
|
+
#wechat-preview-content { width: 100%; max-width: 430px; margin: 0 auto; padding: 18px 16px; box-sizing: border-box; background: #fff; box-shadow: 0 12px 36px rgba(38, 30, 34, 0.10); }
|
|
259
|
+
@media (max-width: 462px) {
|
|
260
|
+
body { padding: 0; background: #fff; }
|
|
261
|
+
#wechat-preview-content { max-width: none; padding: 16px; box-shadow: none; }
|
|
155
262
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
async function ensurePreviewBundleDir(targetDir: string): Promise<void> {
|
|
164
|
-
await mkdir(dirname(targetDir), { recursive: true });
|
|
165
|
-
await rm(targetDir, { recursive: true, force: true });
|
|
166
|
-
await cp(DIST_DIR, targetDir, { recursive: true, force: true });
|
|
263
|
+
</style>
|
|
264
|
+
</head>
|
|
265
|
+
<body>
|
|
266
|
+
<main id="wechat-preview-content">${articleHtml}</main>
|
|
267
|
+
</body>
|
|
268
|
+
</html>`;
|
|
167
269
|
}
|
|
168
270
|
|
|
169
|
-
async function writePreviewShell(
|
|
170
|
-
previewShellOutPath: string
|
|
171
|
-
pageTitle: string
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const previewBundleDir = join(previewDir, "browser-dist");
|
|
180
|
-
await ensurePreviewBundleDir(previewBundleDir);
|
|
181
|
-
|
|
182
|
-
const previewMarkdown = rewriteRelativeImagePathsForPreview(
|
|
183
|
-
options.sourceMarkdown,
|
|
184
|
-
options.sourceMarkdownDir,
|
|
185
|
-
previewDir,
|
|
271
|
+
async function writePreviewShell(
|
|
272
|
+
previewShellOutPath: string,
|
|
273
|
+
pageTitle: string,
|
|
274
|
+
articleHtml: string,
|
|
275
|
+
): Promise<string> {
|
|
276
|
+
await mkdir(dirname(previewShellOutPath), { recursive: true });
|
|
277
|
+
await writeFile(
|
|
278
|
+
previewShellOutPath,
|
|
279
|
+
buildWechatPreviewShell(articleHtml, pageTitle),
|
|
280
|
+
"utf-8",
|
|
186
281
|
);
|
|
187
|
-
|
|
188
|
-
const payloadJson = escapeInlineJson(
|
|
189
|
-
JSON.stringify({
|
|
190
|
-
markdown: previewMarkdown,
|
|
191
|
-
editorVars: options.theme.editorVars,
|
|
192
|
-
exportTheme: options.theme.exportTheme,
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
const relativeScriptPath = relative(previewDir, join(previewBundleDir, "editor-export.js")).replaceAll("\\", "/");
|
|
196
|
-
const relativeCssPath = options.bundle.cssPath
|
|
197
|
-
? relative(previewDir, join(previewBundleDir, relative(DIST_DIR, options.bundle.cssPath))).replaceAll("\\", "/")
|
|
198
|
-
: null;
|
|
199
|
-
|
|
200
|
-
const shellHtml = renderTemplate(shell, {
|
|
201
|
-
"{{PAGE_TITLE}}": escapeHtml(options.pageTitle),
|
|
202
|
-
"{{CSS_LINK}}": relativeCssPath ? `<link rel="stylesheet" href="${relativeCssPath.startsWith(".") ? relativeCssPath : `./${relativeCssPath}`}">` : "",
|
|
203
|
-
"{{PAYLOAD_JSON}}": payloadJson,
|
|
204
|
-
"{{SCRIPT_URL}}": relativeScriptPath.startsWith(".") ? relativeScriptPath : `./${relativeScriptPath}`,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await writeFile(options.previewShellOutPath, shellHtml, "utf-8");
|
|
208
|
-
return options.previewShellOutPath;
|
|
282
|
+
return previewShellOutPath;
|
|
209
283
|
}
|
|
210
284
|
|
|
211
|
-
export
|
|
212
|
-
|
|
213
|
-
|
|
285
|
+
export type ExtractedRenderResult =
|
|
286
|
+
| { status: "success"; html: string; semanticHtml?: string }
|
|
287
|
+
| { status: "error"; error: string }
|
|
288
|
+
| { status: string };
|
|
289
|
+
|
|
290
|
+
export function extractRenderResult(dom: string): ExtractedRenderResult {
|
|
214
291
|
const match = dom.match(
|
|
215
292
|
/<script id="zzhub-wechat-export-result" type="application\/json">([\s\S]*?)<\/script>/,
|
|
216
293
|
);
|
|
@@ -219,93 +296,260 @@ export function extractRenderResult(
|
|
|
219
296
|
throw new Error("Wechat preview result script not found");
|
|
220
297
|
}
|
|
221
298
|
|
|
222
|
-
return JSON.parse(match[1]) as
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
return JSON.parse(match[1]) as ExtractedRenderResult;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function stderrTail(stderr: string, max = 2000): string {
|
|
303
|
+
const trimmed = stderr.trim();
|
|
304
|
+
if (trimmed.length <= max) return trimmed;
|
|
305
|
+
return trimmed.slice(-max);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function writeDebugArtifacts(
|
|
309
|
+
debugDir: string,
|
|
310
|
+
files: Record<string, string | undefined>,
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
await mkdir(debugDir, { recursive: true });
|
|
313
|
+
for (const [name, content] of Object.entries(files)) {
|
|
314
|
+
if (content === undefined) continue;
|
|
315
|
+
await writeFile(join(debugDir, name), content, "utf-8");
|
|
316
|
+
}
|
|
226
317
|
}
|
|
227
318
|
|
|
228
319
|
export async function exportMarkdownToWechatHtml(
|
|
229
320
|
input: ExportMarkdownToWechatHtmlInput,
|
|
230
321
|
): Promise<ExportMarkdownToWechatHtmlResult> {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
" Ubuntu: sudo apt install chromium-browser\n" +
|
|
238
|
-
" Or install Google Chrome from https://www.google.com/chrome/\n" +
|
|
239
|
-
" Or set CHROME_PATH environment variable to the binary path.",
|
|
240
|
-
);
|
|
241
|
-
}
|
|
322
|
+
const started = Date.now();
|
|
323
|
+
const timeoutMs = input.timeoutMs ?? 15_000;
|
|
324
|
+
const title = input.title ?? "Wechat Preview Export";
|
|
325
|
+
let chromePath: string | undefined;
|
|
326
|
+
let bundleRebuilt = false;
|
|
327
|
+
let bundleStale = false;
|
|
242
328
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
329
|
+
const makeError = (
|
|
330
|
+
kind: WechatExportErrorKind,
|
|
331
|
+
message: string,
|
|
332
|
+
debug: WechatExportDebugInfo = {},
|
|
333
|
+
): WechatExportError =>
|
|
334
|
+
new WechatExportError(message, {
|
|
335
|
+
kind,
|
|
336
|
+
durationMs: Date.now() - started,
|
|
337
|
+
account: input.account,
|
|
338
|
+
title,
|
|
339
|
+
markdownPath: input.markdownPath,
|
|
340
|
+
debug: {
|
|
341
|
+
chrome_path: chromePath,
|
|
342
|
+
virtual_time_budget_ms: timeoutMs,
|
|
343
|
+
bundle_stale: bundleStale,
|
|
344
|
+
bundle_rebuilt: bundleRebuilt,
|
|
345
|
+
debug_dir: input.debugDir,
|
|
346
|
+
...debug,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
257
349
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
350
|
+
try {
|
|
351
|
+
const foundChrome = findChrome();
|
|
352
|
+
if (!foundChrome) {
|
|
353
|
+
throw makeError(
|
|
354
|
+
"chrome_missing",
|
|
355
|
+
"Chrome/Chromium not found. Required for WeChat HTML export.\n" +
|
|
356
|
+
"Install one of:\n" +
|
|
357
|
+
" macOS: brew install --cask chromium\n" +
|
|
358
|
+
" Ubuntu: sudo apt install chromium-browser\n" +
|
|
359
|
+
" Or install Google Chrome from https://www.google.com/chrome/\n" +
|
|
360
|
+
" Or set CHROME_PATH environment variable to the binary path.",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
chromePath = foundChrome;
|
|
263
364
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
bundle.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
365
|
+
let bundle: BundleAssets;
|
|
366
|
+
try {
|
|
367
|
+
bundle = ensureBundle();
|
|
368
|
+
bundleRebuilt = bundle.rebuilt;
|
|
369
|
+
bundleStale = bundle.stale;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw makeError(
|
|
372
|
+
"bundle",
|
|
373
|
+
error instanceof Error ? error.message : String(error),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
275
376
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
377
|
+
const sourceMarkdown = await readFile(input.markdownPath, "utf-8");
|
|
378
|
+
const stripped = extractFrontmatter(sourceMarkdown).content;
|
|
379
|
+
const bodyMarkdown = stripLeadingH1(stripped).trimStart();
|
|
380
|
+
const markdown = rewriteRelativeImagePaths(bodyMarkdown, dirname(input.markdownPath));
|
|
381
|
+
const theme = getWechatPreviewTheme(input.account, input.themeOverrides);
|
|
382
|
+
const customCss = input.customCss
|
|
383
|
+
? await readFile(input.customCss, "utf-8")
|
|
384
|
+
: undefined;
|
|
385
|
+
const shell = readUtf8(TEMPLATE_PATH);
|
|
386
|
+
const payloadJson = escapeInlineJson(
|
|
387
|
+
JSON.stringify({
|
|
388
|
+
markdown,
|
|
389
|
+
editorVars: theme.editorVars,
|
|
390
|
+
exportTheme: theme.exportTheme,
|
|
391
|
+
customCss,
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
288
394
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
previewShellPath = await writePreviewShell({
|
|
295
|
-
previewShellOutPath: input.previewShellOutPath,
|
|
296
|
-
pageTitle: input.title ?? "Wechat Preview Export",
|
|
297
|
-
sourceMarkdown: bodyMarkdown,
|
|
298
|
-
sourceMarkdownDir: dirname(input.markdownPath),
|
|
299
|
-
theme,
|
|
300
|
-
bundle,
|
|
395
|
+
const shellHtml = renderTemplate(shell, {
|
|
396
|
+
"{{PAGE_TITLE}}": escapeHtml(title),
|
|
397
|
+
"{{CSS_LINK}}": "",
|
|
398
|
+
"{{PAYLOAD_JSON}}": payloadJson,
|
|
399
|
+
"{{SCRIPT_URL}}": pathToFileURL(bundle.scriptPath).href,
|
|
301
400
|
});
|
|
302
|
-
}
|
|
303
401
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
402
|
+
let dumpedDom: string;
|
|
403
|
+
try {
|
|
404
|
+
dumpedDom = dumpHtmlDom({
|
|
405
|
+
chromePath,
|
|
406
|
+
html: shellHtml,
|
|
407
|
+
virtualTimeBudgetMs: timeoutMs,
|
|
408
|
+
keepTempOnError: Boolean(input.debugDir),
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error instanceof ChromeDumpError) {
|
|
412
|
+
if (input.debugDir) {
|
|
413
|
+
await writeDebugArtifacts(input.debugDir, {
|
|
414
|
+
"00-payload-markdown.md": markdown,
|
|
415
|
+
"01-export-shell.html": shellHtml,
|
|
416
|
+
"meta.json": JSON.stringify({
|
|
417
|
+
status: "failed",
|
|
418
|
+
kind: "chrome_failed",
|
|
419
|
+
error: error.message,
|
|
420
|
+
stderr: error.stderr,
|
|
421
|
+
duration_ms: Date.now() - started,
|
|
422
|
+
}, null, 2),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
throw makeError("chrome_failed", error.message, {
|
|
426
|
+
shell_path: error.tempHtmlPath,
|
|
427
|
+
stderr_tail: stderrTail(error.stderr),
|
|
428
|
+
debug_dir: input.debugDir,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
throw makeError(
|
|
432
|
+
"chrome_failed",
|
|
433
|
+
error instanceof Error ? error.message : String(error),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let result: ExtractedRenderResult;
|
|
438
|
+
try {
|
|
439
|
+
result = extractRenderResult(dumpedDom);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (input.debugDir) {
|
|
442
|
+
await writeDebugArtifacts(input.debugDir, {
|
|
443
|
+
"00-payload-markdown.md": markdown,
|
|
444
|
+
"01-export-shell.html": shellHtml,
|
|
445
|
+
"02-dump-dom.html": dumpedDom,
|
|
446
|
+
"meta.json": JSON.stringify({
|
|
447
|
+
status: "failed",
|
|
448
|
+
kind: "timeout",
|
|
449
|
+
error: error instanceof Error ? error.message : String(error),
|
|
450
|
+
duration_ms: Date.now() - started,
|
|
451
|
+
}, null, 2),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
throw makeError(
|
|
455
|
+
"timeout",
|
|
456
|
+
`Wechat preview export did not finish before timeout (${timeoutMs}ms): ${
|
|
457
|
+
error instanceof Error ? error.message : String(error)
|
|
458
|
+
}`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (result.status === "error" && "error" in result) {
|
|
463
|
+
if (input.debugDir) {
|
|
464
|
+
await writeDebugArtifacts(input.debugDir, {
|
|
465
|
+
"00-payload-markdown.md": markdown,
|
|
466
|
+
"01-export-shell.html": shellHtml,
|
|
467
|
+
"02-dump-dom.html": dumpedDom,
|
|
468
|
+
"meta.json": JSON.stringify({
|
|
469
|
+
status: "failed",
|
|
470
|
+
kind: "render_error",
|
|
471
|
+
error: result.error,
|
|
472
|
+
duration_ms: Date.now() - started,
|
|
473
|
+
}, null, 2),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
throw makeError("render_error", `Wechat preview export failed: ${result.error}`);
|
|
477
|
+
}
|
|
478
|
+
if (result.status !== "success" || !("html" in result)) {
|
|
479
|
+
if (input.debugDir) {
|
|
480
|
+
await writeDebugArtifacts(input.debugDir, {
|
|
481
|
+
"00-payload-markdown.md": markdown,
|
|
482
|
+
"01-export-shell.html": shellHtml,
|
|
483
|
+
"02-dump-dom.html": dumpedDom,
|
|
484
|
+
"meta.json": JSON.stringify({
|
|
485
|
+
status: "failed",
|
|
486
|
+
kind: "timeout",
|
|
487
|
+
status_value: result.status,
|
|
488
|
+
duration_ms: Date.now() - started,
|
|
489
|
+
}, null, 2),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
throw makeError(
|
|
493
|
+
"timeout",
|
|
494
|
+
`Wechat preview export did not finish before timeout (status=${result.status}, budget=${timeoutMs}ms)`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const html = result.html;
|
|
499
|
+
const semanticHtml =
|
|
500
|
+
"semanticHtml" in result && typeof result.semanticHtml === "string"
|
|
501
|
+
? result.semanticHtml
|
|
502
|
+
: undefined;
|
|
503
|
+
|
|
504
|
+
ensureParentDir(input.outPath);
|
|
505
|
+
await writeFile(input.outPath, html, "utf-8");
|
|
506
|
+
|
|
507
|
+
let previewShellPath: string | undefined;
|
|
508
|
+
if (input.previewShellOutPath) {
|
|
509
|
+
previewShellPath = await writePreviewShell(
|
|
510
|
+
input.previewShellOutPath,
|
|
511
|
+
title,
|
|
512
|
+
html,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const durationMs = Date.now() - started;
|
|
517
|
+
if (input.debugDir) {
|
|
518
|
+
await writeDebugArtifacts(input.debugDir, {
|
|
519
|
+
"00-payload-markdown.md": markdown,
|
|
520
|
+
"01-export-shell.html": shellHtml,
|
|
521
|
+
"02-dump-dom.html": dumpedDom,
|
|
522
|
+
"03-article.html": html,
|
|
523
|
+
"03-semantic.html": semanticHtml,
|
|
524
|
+
"04-preview-shell.html": previewShellPath
|
|
525
|
+
? await readFile(previewShellPath, "utf-8")
|
|
526
|
+
: buildWechatPreviewShell(html, title),
|
|
527
|
+
"meta.json": JSON.stringify({
|
|
528
|
+
status: "success",
|
|
529
|
+
account: input.account,
|
|
530
|
+
duration_ms: durationMs,
|
|
531
|
+
bundle_rebuilt: bundleRebuilt,
|
|
532
|
+
bundle_stale: bundleStale,
|
|
533
|
+
timeout_ms: timeoutMs,
|
|
534
|
+
html_path: input.outPath,
|
|
535
|
+
}, null, 2),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
html,
|
|
541
|
+
htmlPath: input.outPath,
|
|
542
|
+
account: input.account,
|
|
543
|
+
previewStyle: getWechatPreviewStyleName(input.account),
|
|
544
|
+
previewShellPath,
|
|
545
|
+
durationMs,
|
|
546
|
+
bundleRebuilt,
|
|
547
|
+
bundleStale,
|
|
548
|
+
debugDir: input.debugDir,
|
|
549
|
+
semanticHtml,
|
|
550
|
+
};
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof WechatExportError) throw error;
|
|
553
|
+
throw makeError("other", error instanceof Error ? error.message : String(error));
|
|
554
|
+
}
|
|
311
555
|
}
|