@zzclub/pipeline 0.4.0 → 0.9.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 (48) hide show
  1. package/README.md +102 -3
  2. package/package.json +1 -1
  3. package/src/adapter-types.ts +6 -0
  4. package/src/adapters/builtin-image-renderer.ts +7 -1
  5. package/src/adapters/builtin-markdown-renderer.ts +1 -0
  6. package/src/commands/analytics.test.ts +155 -0
  7. package/src/commands/analytics.ts +223 -0
  8. package/src/commands/init.ts +42 -3
  9. package/src/commands/publish.ts +44 -67
  10. package/src/commands/republish.ts +146 -0
  11. package/src/commands/sync-blog.ts +19 -3
  12. package/src/commands/topic.test.ts +146 -0
  13. package/src/commands/topic.ts +717 -0
  14. package/src/commands/wechat-export.ts +2 -0
  15. package/src/commands/wx-draft-delete.ts +36 -0
  16. package/src/commands/wx-drafts.ts +47 -0
  17. package/src/config.ts +0 -0
  18. package/src/db.test.ts +47 -0
  19. package/src/db.ts +95 -0
  20. package/src/imgx/assets/icons/ancientone-logo.png +0 -0
  21. package/src/imgx/assets/templates/wechat-cover-split.html +1 -1
  22. package/src/imgx/render-card.ts +1 -5
  23. package/src/plugins.ts +11 -1
  24. package/src/providers/blog.ts +9 -3
  25. package/src/providers/index.test.ts +1 -1
  26. package/src/providers/index.ts +29 -5
  27. package/src/providers/publish-core.ts +124 -0
  28. package/src/providers/wechat.ts +260 -28
  29. package/src/routes.ts +1 -1
  30. package/src/schema/analytics.test.ts +54 -0
  31. package/src/schema/analytics.ts +19 -0
  32. package/src/schema/config.ts +25 -1
  33. package/src/schema/state.ts +10 -0
  34. package/src/schema/topic.test.ts +69 -0
  35. package/src/schema/topic.ts +55 -0
  36. package/src/state.ts +1 -0
  37. package/src/text.test.ts +11 -6
  38. package/src/text.ts +27 -9
  39. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +2 -7
  40. package/src/wechat-preview/assets/browser-dist/assets/{style-BYNXnUvc.css → style-vhHYSP8T.css} +1 -1
  41. package/src/wechat-preview/assets/browser-dist/editor-export.js +27 -27
  42. package/src/wechat-preview/browser/editor-export.css +248 -159
  43. package/src/wechat-preview/index.ts +28 -11
  44. package/src/wechat-preview/themes.ts +70 -24
  45. package/src/wechat-preview/wechat-formatter.test.ts +0 -1
  46. package/src/wechat-preview/wechat-formatter.ts +48 -23
  47. package/src/workflow.test.ts +294 -1
  48. package/src/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
@@ -1,241 +1,330 @@
1
- @font-face {
2
- font-family: "SweiCurveLeg";
3
- src: url("../assets/fonts/SweiCurveLegCJKsc-Regular.woff") format("woff");
4
- font-weight: normal;
5
- font-style: normal;
6
- font-display: swap;
7
- }
1
+ /*
2
+ * WeChat Article Typography — 温暖人文
3
+ * Brand: #ca6093 (玫瑰暖调)
4
+ *
5
+ * Design tokens (:root custom properties) anchor the numeric scale.
6
+ * All values derive from:
7
+ * --font-base = 16px (body)
8
+ * --leading = 2.0 (body line-height ratio)
9
+ * --brand = #ca6093
10
+ * --text = #2c2c2c (warm black)
11
+ *
12
+ * When converting to WeChat inline styles, resolve each var() to its
13
+ * computed value. The token layer makes future rebranding a one-line edit.
14
+ */
8
15
 
9
16
  :root {
10
- color-scheme: light;
11
- }
17
+ /* ── Core tokens ── */
18
+ --font-base: 16px;
19
+ --font-sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
20
+ -apple-system, "SF Pro Text", "Noto Sans SC", sans-serif;
21
+ --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", "Consolas",
22
+ "Monaco", "Courier New", monospace;
23
+ --font-serif: "Noto Serif SC", "Source Han Serif SC", "Songti SC",
24
+ "STSong", "Georgia", serif;
12
25
 
13
- html,
14
- body {
15
- margin: 0;
16
- min-height: 100%;
17
- background: hsl(var(--background));
18
- }
26
+ --leading: 2.0;
19
27
 
20
- body {
21
- color: hsl(var(--foreground));
22
- font-family: "SweiCurveLeg", ui-sans-serif, system-ui, sans-serif;
23
- }
28
+ --brand: #ca6093;
29
+ --brand-soft: #d4789e; /* hover / secondary accent */
30
+ --brand-bg: rgba(202, 96, 147, 0.08);
31
+ --brand-bg-deep: rgba(202, 96, 147, 0.14);
24
32
 
25
- .preview-root {
26
- min-height: 100vh;
27
- padding: 40px 0;
28
- background: hsl(var(--background));
29
- }
33
+ --text: #2c2c2c; /* warm black, never pure #000 */
34
+ --text-soft: #8c8c8c; /* captions, meta */
35
+ --text-mid: #5c5c5c; /* blockquote, secondary text */
30
36
 
31
- .milkdown-container {
32
- min-height: 100%;
33
- width: 100%;
34
- --crepe-color-background: hsl(var(--background));
35
- --crepe-color-surface: hsl(var(--card));
36
- --crepe-color-surface-low: hsl(var(--muted));
37
- --crepe-color-on-background: hsl(var(--foreground));
38
- --crepe-color-on-surface: hsl(var(--card-foreground));
39
- --crepe-color-outline: hsl(var(--border));
40
- --crepe-color-primary: hsl(var(--primary));
41
- --crepe-color-on-primary: hsl(var(--primary-foreground));
37
+ --border: #e8e4e5; /* warm gray */
38
+ --divider: #d8d0d3; /* slightly warmer than border */
39
+ --bg-page: #fefefe;
40
+ --bg-warm: #faf8f9; /* subtle warm tint for blockquote */
41
+
42
+ --radius-sm: 3px;
43
+ --radius-md: 6px;
42
44
  }
43
45
 
46
+ /* ══════════════════════════════════════════════════════════════════════
47
+ Container
48
+ ══════════════════════════════════════════════════════════════════════ */
49
+
44
50
  .milkdown .editor {
45
- min-height: 100%;
46
- max-width: 900px;
51
+ max-width: 680px;
47
52
  margin: 0 auto;
48
- padding: 0 32px 10vh;
49
- font-family: "SweiCurveLeg", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
50
- font-size: 16px;
51
- line-height: 1.75;
52
- letter-spacing: 0.01em;
53
- background: transparent;
53
+ padding: 48px 24px 80px;
54
+ font-family: var(--font-sans);
55
+ font-size: var(--font-base);
56
+ line-height: var(--leading);
57
+ color: var(--text);
54
58
  text-rendering: optimizeLegibility;
55
59
  -webkit-font-smoothing: antialiased;
60
+ -moz-osx-font-smoothing: grayscale;
56
61
  }
57
62
 
58
- .milkdown .editor,
59
- .milkdown .editor * {
60
- box-sizing: border-box;
63
+ /* ══════════════════════════════════════════════════════════════════════
64
+ Paragraph
65
+ ══════════════════════════════════════════════════════════════════════ */
66
+
67
+ .milkdown .editor p {
68
+ margin: 0 0 1.4em 0;
69
+ line-height: var(--leading);
70
+ text-align: justify;
71
+ hyphens: auto;
72
+ word-break: break-all;
73
+ overflow-wrap: break-word;
61
74
  }
62
75
 
63
- .milkdown h1 {
64
- font-size: 2.25rem;
65
- font-weight: 700;
66
- line-height: 1.2;
67
- margin-top: 2rem;
68
- margin-bottom: 1rem;
69
- letter-spacing: -0.02em;
76
+ .milkdown .editor p:first-child {
77
+ margin-top: 0;
70
78
  }
71
79
 
72
- .milkdown .editor h2 {
73
- font-size: 18px;
80
+ .milkdown .editor p:last-child {
81
+ margin-bottom: 0;
82
+ }
83
+
84
+ /* ══════════════════════════════════════════════════════════════════════
85
+ Headings
86
+ ══════════════════════════════════════════════════════════════════════ */
87
+
88
+ .milkdown .editor h1 {
89
+ font-size: 1.625rem; /* 26px */
74
90
  font-weight: 700;
75
- color: hsl(var(--primary));
76
91
  line-height: 1.3;
77
- margin-top: 1.75rem;
78
- margin-bottom: 0.75rem;
92
+ margin: 3em 0 1em 0;
93
+ color: var(--text);
79
94
  letter-spacing: -0.01em;
80
95
  }
81
96
 
82
- .milkdown .editor h3 {
83
- font-size: 18px;
84
- font-weight: 400;
85
- line-height: 1.4;
86
- margin-top: 1.5rem;
87
- margin-bottom: 0.5rem;
88
- }
97
+ /* ── h2: brand color, clean style ── */
89
98
 
90
- .milkdown h4 {
91
- font-size: 1.125rem;
92
- font-weight: 600;
93
- line-height: 1.5;
94
- margin-top: 1.25rem;
95
- margin-bottom: 0.5rem;
99
+ .milkdown .editor h2 {
100
+ font-size: 1.25rem; /* 20px */
101
+ font-weight: 700;
102
+ line-height: 1.35;
103
+ margin: 2.4em 0 0.9em 0;
104
+ color: var(--brand);
105
+ letter-spacing: 0.02em;
96
106
  }
97
107
 
98
- .milkdown h5,
99
- .milkdown h6 {
100
- font-size: 1rem;
108
+ /* ── h3: warm black, semi-bold, underlined by a thin rule ── */
109
+
110
+ .milkdown .editor h3 {
111
+ font-size: 1.125rem; /* 18px */
101
112
  font-weight: 600;
102
- line-height: 1.5;
103
- margin-top: 1rem;
104
- margin-bottom: 0.5rem;
113
+ line-height: 1.45;
114
+ margin: 2em 0 0.65em 0;
115
+ padding-bottom: 0.5em;
116
+ color: var(--text);
117
+ letter-spacing: 0.015em;
118
+ border-bottom: 1px solid var(--border);
105
119
  }
106
120
 
107
- .milkdown .editor p {
108
- line-height: 1.9;
109
- margin: 1.25rem 0;
110
- color: hsl(var(--foreground));
111
- }
121
+ /* ══════════════════════════════════════════════════════════════════════
122
+ Bold — brand-colored emphasis
123
+ ══════════════════════════════════════════════════════════════════════ */
112
124
 
113
- .milkdown .editor p:first-child {
114
- margin-top: 0;
125
+ .milkdown .editor strong,
126
+ .milkdown .editor b {
127
+ font-weight: 700;
128
+ color: var(--text);
115
129
  }
116
130
 
117
- .milkdown .editor p:last-child {
118
- margin-bottom: 0;
119
- }
131
+ /* ══════════════════════════════════════════════════════════════════════
132
+ Links
133
+ ══════════════════════════════════════════════════════════════════════ */
120
134
 
121
- .milkdown .editor strong,
122
- .milkdown .editor b,
123
- .milkdown .editor em,
124
- .milkdown .editor i,
125
- .milkdown .editor a,
126
- .milkdown .editor del,
127
- .milkdown .editor span {
128
- font-size: inherit;
129
- line-height: inherit;
130
- letter-spacing: inherit;
135
+ .milkdown .editor a {
136
+ color: var(--brand);
137
+ text-decoration: none;
138
+ border-bottom: 1px solid var(--brand-soft);
139
+ transition: border-color 0.15s;
131
140
  }
132
141
 
133
- .milkdown .editor blockquote {
134
- margin: 1.35rem 0;
135
- padding-left: 1rem;
136
- border-left: 4px solid hsl(var(--border));
137
- background: color-mix(in srgb, hsl(var(--primary)) 5%, white);
142
+ .milkdown .editor a:hover {
143
+ border-bottom-color: var(--brand);
138
144
  }
139
145
 
140
- .milkdown .editor blockquote p {
141
- font-size: 15px;
142
- line-height: 1.85;
143
- color: hsl(var(--muted-foreground));
144
- margin: 0.45rem 0;
145
- }
146
+ /* ══════════════════════════════════════════════════════════════════════
147
+ Inline code
148
+ ══════════════════════════════════════════════════════════════════════ */
146
149
 
147
- .milkdown .editor ul,
148
- .milkdown .editor ol {
149
- margin: 1rem 0;
150
- padding-left: 1.4rem;
150
+ .milkdown .editor code {
151
+ font-family: var(--font-mono);
152
+ font-size: 0.875em;
153
+ color: var(--brand);
154
+ background: var(--brand-bg);
155
+ padding: 2px 6px;
156
+ border-radius: var(--radius-sm);
157
+ word-break: break-all;
158
+ }
159
+
160
+ .milkdown .editor pre {
161
+ background: #f5f2f3;
162
+ border: 1px solid var(--border);
163
+ border-radius: var(--radius-md);
164
+ padding: 1.2em 1.3em;
165
+ margin: 1.5em 0;
166
+ overflow-x: auto;
167
+ line-height: 1.65;
151
168
  }
152
169
 
153
- .milkdown .editor li {
154
- margin: 0.45rem 0;
155
- line-height: 1.85;
170
+ .milkdown .editor pre code {
171
+ background: none;
172
+ padding: 0;
173
+ font-size: 0.875em;
174
+ color: var(--text);
156
175
  }
157
176
 
158
- .milkdown .editor li > p {
159
- margin: 0.2rem 0;
177
+ /* ══════════════════════════════════════════════════════════════════════
178
+ Image caption
179
+ ══════════════════════════════════════════════════════════════════════ */
180
+
181
+ .milkdown .editor figure {
182
+ margin: 1.6em auto;
183
+ text-align: center;
160
184
  }
161
185
 
162
- .milkdown .editor code {
163
- font-family: "JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace;
164
- font-size: 0.875em;
165
- background: hsl(var(--primary) / 0.12);
166
- color: var(--crepe-color-primary);
167
- padding: 1px 2px;
168
- border-radius: 4px;
186
+ .milkdown .editor figure img {
187
+ display: block;
188
+ max-width: 100%;
189
+ margin: 0 auto;
190
+ border-radius: var(--radius-sm);
169
191
  }
170
192
 
171
- .milkdown pre {
172
- background: hsl(var(--muted));
173
- border-radius: 8px;
174
- padding: 1rem;
175
- margin: 1.5rem 0;
176
- overflow-x: auto;
193
+ .milkdown .editor figure figcaption,
194
+ .milkdown .editor .img-caption {
195
+ display: block;
196
+ margin-top: 10px;
197
+ font-size: 0.875rem; /* 14px */
177
198
  line-height: 1.6;
199
+ color: var(--text-soft);
200
+ text-align: center;
178
201
  }
179
202
 
180
- .milkdown pre code {
181
- background: none;
182
- padding: 0;
183
- font-size: 0.9em;
203
+ /* For standalone images (not wrapped in figure) */
204
+ .milkdown .editor p:has(img) + p.img-caption,
205
+ .milkdown .editor img + .img-caption {
206
+ display: block;
207
+ margin-top: 8px;
208
+ font-size: 0.875rem;
209
+ color: var(--text-soft);
210
+ text-align: center;
184
211
  }
185
212
 
186
- .milkdown .editor a {
187
- color: hsl(var(--primary));
188
- text-decoration: underline;
189
- text-decoration-style: wavy;
190
- text-underline-offset: 3px;
213
+ /* Standalone image between paragraphs */
214
+ .milkdown .editor p img {
215
+ display: block;
216
+ max-width: 100%;
217
+ margin: 0 auto;
191
218
  }
192
219
 
193
- .milkdown hr {
220
+ /* ══════════════════════════════════════════════════════════════════════
221
+ Divider
222
+ ══════════════════════════════════════════════════════════════════════ */
223
+
224
+ .milkdown .editor hr {
194
225
  display: block;
195
226
  height: 0;
196
227
  border: none;
197
- border-top: 1px solid hsl(var(--border));
198
- margin: 1.8rem 0;
228
+ margin: 2.2em 0;
229
+ text-align: center;
230
+ /* thin warm line */
231
+ border-top: 1px solid var(--divider);
199
232
  padding: 0;
200
233
  background: transparent;
201
- opacity: 0.72;
202
234
  }
203
235
 
236
+ /*
237
+ * Decorative centered-dot divider variant.
238
+ * Override by adding class="hr-dots" on the <hr>.
239
+ */
240
+ .milkdown .editor hr.hr-dots {
241
+ border-top: none;
242
+ line-height: 1;
243
+ letter-spacing: 0.6em;
244
+ color: var(--text-soft);
245
+ font-size: 0.75rem;
246
+ }
247
+
248
+ .milkdown .editor hr.hr-dots::after {
249
+ content: "· · ·";
250
+ }
251
+
252
+ /* ══════════════════════════════════════════════════════════════════════
253
+ Blockquote
254
+ ══════════════════════════════════════════════════════════════════════ */
255
+
256
+ .milkdown .editor blockquote {
257
+ margin: 1.5em 0;
258
+ padding: 0.8em 1em 0.8em 1.15em;
259
+ border-left: 3px solid var(--brand-soft);
260
+ background: var(--bg-warm);
261
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
262
+ }
263
+
264
+ .milkdown .editor blockquote p {
265
+ font-size: 0.9375rem; /* 15px */
266
+ line-height: 1.85;
267
+ color: var(--text-mid);
268
+ margin: 0.45em 0;
269
+ }
270
+
271
+ /* ══════════════════════════════════════════════════════════════════════
272
+ Lists
273
+ ══════════════════════════════════════════════════════════════════════ */
274
+
275
+ .milkdown .editor ul,
276
+ .milkdown .editor ol {
277
+ margin: 1.1em 0;
278
+ padding-left: 1.5em;
279
+ }
280
+
281
+ .milkdown .editor li {
282
+ margin: 0.5em 0;
283
+ line-height: 1.85;
284
+ }
285
+
286
+ .milkdown .editor li > p {
287
+ margin: 0.2em 0;
288
+ }
289
+
290
+ /* ══════════════════════════════════════════════════════════════════════
291
+ Tables
292
+ ══════════════════════════════════════════════════════════════════════ */
293
+
204
294
  .milkdown .editor table {
205
295
  width: 100%;
206
- margin: 1.4rem 0;
296
+ margin: 1.4em 0;
207
297
  border-collapse: collapse;
208
- font-size: 15px;
298
+ font-size: 0.9375rem;
209
299
  line-height: 1.75;
210
300
  }
211
301
 
212
302
  .milkdown .editor th,
213
303
  .milkdown .editor td {
214
- padding: 10px 12px;
215
- border: 1px solid hsl(var(--border));
304
+ padding: 10px 14px;
305
+ border: 1px solid var(--border);
216
306
  text-align: left;
217
307
  vertical-align: top;
218
308
  }
219
309
 
220
310
  .milkdown .editor th {
221
- color: hsl(var(--primary));
222
- background: color-mix(in srgb, hsl(var(--primary)) 6%, white);
311
+ color: var(--text);
312
+ background: var(--brand-bg);
223
313
  font-weight: 700;
224
314
  }
225
315
 
226
- .milkdown .editor img {
227
- display: block;
228
- max-width: 100%;
229
- margin: 14px auto;
230
- }
316
+ /* ══════════════════════════════════════════════════════════════════════
317
+ Responsive
318
+ ══════════════════════════════════════════════════════════════════════ */
231
319
 
232
320
  @media (max-width: 768px) {
233
- .preview-root {
234
- padding: 20px 0;
235
- }
236
-
237
321
  .milkdown .editor {
238
- padding: 0 20px 8vh;
322
+ max-width: 100%;
323
+ padding: 28px 20px 64px;
239
324
  font-size: 15px;
240
325
  }
326
+
327
+ .milkdown .editor h1 { font-size: 1.4rem; }
328
+ .milkdown .editor h2 { font-size: 1.15rem; }
329
+ .milkdown .editor h3 { font-size: 1.05rem; }
241
330
  }
@@ -38,6 +38,10 @@ export interface ExportMarkdownToWechatHtmlInput {
38
38
  title?: string;
39
39
  previewShellOutPath?: string;
40
40
  customCss?: string | null;
41
+ themeOverrides?: {
42
+ editorVars?: Record<string, string>;
43
+ exportTheme?: Record<string, string>;
44
+ };
41
45
  }
42
46
 
43
47
  export interface ExportMarkdownToWechatHtmlResult {
@@ -103,12 +107,17 @@ export function resolveMarkdownAsset(rawPath: string, baseDir: string): string {
103
107
 
104
108
  export function rewriteRelativeImagePaths(markdown: string, baseDir: string): string {
105
109
  return markdown
106
- .replace(/!\[([^\]]*)\]\((<)?([^)]+?)(>)?\)/g, (_match, alt, open, src) => {
107
- const resolved = resolveMarkdownAsset(String(src ?? ""), baseDir);
110
+ .replace(/!\[([^\]]*)\]\((?:<([^>]+)>|([^)]+?))(?:\s+(["'])([^"']*)\4)?\)/g, (_match, alt, angleUrl, plainUrl, _quote, title) => {
111
+ const src = angleUrl ?? plainUrl ?? "";
112
+ const hasAngleBrackets = angleUrl !== undefined;
113
+ const resolved = resolveMarkdownAsset(src, baseDir);
114
+ const needsBrackets = resolved ? resolved.includes(" ") : src.includes(" ");
115
+ const titleStr = title ? ` "${title}"` : "";
108
116
  if (!resolved || resolved === src) {
109
- return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
117
+ const wrap = !!(hasAngleBrackets || needsBrackets);
118
+ return `![${alt}](${wrap ? "<" : ""}${src}${wrap ? ">" : ""}${titleStr})`;
110
119
  }
111
- return `![${alt}](<${resolved}>)`;
120
+ return `![${alt}](${needsBrackets ? "<" : ""}${resolved}${needsBrackets ? ">" : ""}${titleStr})`;
112
121
  })
113
122
  .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
114
123
  const resolved = resolveMarkdownAsset(String(src ?? ""), baseDir);
@@ -118,18 +127,26 @@ export function rewriteRelativeImagePaths(markdown: string, baseDir: string): st
118
127
 
119
128
  function rewriteRelativeImagePathsForPreview(markdown: string, baseDir: string, previewDir: string): string {
120
129
  return markdown
121
- .replace(/!\[([^\]]*)\]\((<)?([^)]+?)(>)?\)/g, (_match, alt, open, src) => {
122
- const raw = String(src ?? "").trim();
130
+ .replace(/!\[([^\]]*)\]\((?:<([^>]+)>|([^)]+?))(?:\s+(["'])([^"']*)\4)?\)/g, (_match, alt, angleUrl, plainUrl, _quote, title) => {
131
+ const src = angleUrl ?? plainUrl ?? "";
132
+ const hasAngleBrackets = angleUrl !== undefined;
133
+ const raw = src.trim();
123
134
  if (!raw || isExternalUrl(raw)) {
124
- return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
135
+ const wrap = !!(hasAngleBrackets || raw.includes(" "));
136
+ const titleStr = title ? ` "${title}"` : "";
137
+ return `![${alt}](${wrap ? "<" : ""}${src}${wrap ? ">" : ""}${titleStr})`;
125
138
  }
126
139
  const resolved = resolveMarkdownAsset(raw, baseDir);
127
140
  if (!resolved) {
128
- return `![${alt}](${open ? "<" : ""}${src}${open ? ">" : ""})`;
141
+ const wrap = !!(hasAngleBrackets || raw.includes(" "));
142
+ const titleStr = title ? ` "${title}"` : "";
143
+ return `![${alt}](${wrap ? "<" : ""}${src}${wrap ? ">" : ""}${titleStr})`;
129
144
  }
130
145
  const previewRelative = relative(previewDir, resolved).replaceAll("\\", "/");
131
146
  const normalized = previewRelative.startsWith(".") ? previewRelative : `./${previewRelative}`;
132
- return `![${alt}](${open ? "<" : ""}${normalized}${open ? ">" : ""})`;
147
+ const wrap = !!(hasAngleBrackets || normalized.includes(" "));
148
+ const titleStr = title ? ` "${title}"` : "";
149
+ return `![${alt}](${wrap ? "<" : ""}${normalized}${wrap ? ">" : ""}${titleStr})`;
133
150
  })
134
151
  .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
135
152
  const raw = String(src ?? "").trim();
@@ -226,9 +243,9 @@ export async function exportMarkdownToWechatHtml(
226
243
  const bundle = ensureBundle();
227
244
  const sourceMarkdown = await readFile(input.markdownPath, "utf-8");
228
245
  const stripped = extractFrontmatter(sourceMarkdown).content;
229
- const bodyMarkdown = stripLeadingH1(stripped);
246
+ const bodyMarkdown = stripLeadingH1(stripped).trimStart();
230
247
  const markdown = rewriteRelativeImagePaths(bodyMarkdown, dirname(input.markdownPath));
231
- const theme = getWechatPreviewTheme(input.account);
248
+ const theme = getWechatPreviewTheme(input.account, input.themeOverrides);
232
249
  const shell = readUtf8(TEMPLATE_PATH);
233
250
  const payloadJson = escapeInlineJson(
234
251
  JSON.stringify({