@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.
- package/README.md +366 -170
- package/package.json +4 -7
- package/src/adapter-loader.ts +138 -0
- package/src/adapter-types.ts +172 -0
- package/src/adapters/builtin-image-renderer.ts +353 -0
- package/src/adapters/builtin-markdown-renderer.ts +63 -0
- package/src/adapters/index.ts +2 -0
- package/src/args.test.ts +118 -0
- package/src/args.ts +90 -0
- package/src/cli.ts +51 -0
- package/src/commands/abandon.test.ts +104 -0
- package/src/commands/abandon.ts +202 -0
- package/src/commands/attach-body-images.ts +88 -0
- package/src/commands/attach-body.ts +86 -0
- package/src/commands/attach-newspic-spec.ts +41 -0
- package/src/commands/checkpoint.test.ts +91 -0
- package/src/commands/checkpoint.ts +66 -0
- package/src/commands/config.ts +106 -0
- package/src/commands/cos-upload.ts +62 -0
- package/src/commands/doctor.ts +66 -0
- package/src/commands/find-run.ts +55 -0
- package/src/commands/hermes-metrics.ts +226 -0
- package/src/commands/imgx.ts +60 -0
- package/src/commands/ingest-handoff.ts +509 -0
- package/src/commands/init.ts +126 -0
- package/src/commands/prepare-finalize.ts +348 -0
- package/src/commands/prepare.ts +310 -0
- package/src/commands/publish.ts +147 -0
- package/src/commands/reconcile.ts +36 -0
- package/src/commands/render.ts +244 -0
- package/src/commands/reset.test.ts +151 -0
- package/src/commands/reset.ts +159 -0
- package/src/commands/review.ts +77 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/sync-blog.ts +39 -0
- package/src/commands/tasks.ts +50 -0
- package/src/commands/wechat-export.ts +68 -0
- package/src/config.ts +0 -0
- package/src/imgx/ascii-portrait.ts +77 -0
- package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
- package/src/imgx/assets/icons/logo.png +0 -0
- package/src/imgx/assets/icons/logo.svg +8 -0
- package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
- package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
- package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
- package/src/imgx/cli.ts +57 -0
- package/src/imgx/geometry.ts +68 -0
- package/src/imgx/index.ts +5 -0
- package/src/imgx/longform-theme.ts +243 -0
- package/src/imgx/poster-recipe.ts +81 -0
- package/src/imgx/pretext-adapter.ts +14 -0
- package/src/imgx/pretext-runtime.ts +61 -0
- package/src/imgx/references/ascii-portrait-3-4.md +59 -0
- package/src/imgx/references/longform-3-4.md +173 -0
- package/src/imgx/references/poster-3-4.md +87 -0
- package/src/imgx/references/tips-3-4.md +69 -0
- package/src/imgx/references/wechat-cover-split.md +89 -0
- package/src/imgx/references/x-like-posts.md +139 -0
- package/src/imgx/render-article.test.ts +53 -0
- package/src/imgx/render-article.ts +1364 -0
- package/src/imgx/render-ascii-portrait.ts +53 -0
- package/src/imgx/render-card.ts +232 -0
- package/src/imgx/render-x-like-posts.ts +235 -0
- package/src/imgx/runtime.ts +403 -0
- package/src/imgx/typographic-scale.test.ts +56 -0
- package/src/imgx/typographic-scale.ts +44 -0
- package/src/output.ts +632 -0
- package/src/plugins.ts +101 -0
- package/src/profiles.ts +149 -0
- package/src/providers/blog.test.ts +58 -0
- package/src/providers/blog.ts +154 -0
- package/src/providers/cos.ts +183 -0
- package/src/providers/index.test.ts +26 -0
- package/src/providers/index.ts +185 -0
- package/src/providers/wechat.test.ts +215 -0
- package/src/providers/wechat.ts +548 -0
- package/src/routes.ts +287 -0
- package/src/runtime-paths.ts +179 -0
- package/src/schema/config.ts +133 -0
- package/src/schema/state.ts +376 -0
- package/src/spawn.ts +103 -0
- package/src/state.ts +265 -0
- package/src/task-manager.ts +657 -0
- package/src/task-views.ts +222 -0
- package/src/text.test.ts +689 -0
- package/src/text.ts +683 -0
- package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
- package/src/wechat-preview/browser/editor-export.css +241 -0
- package/src/wechat-preview/browser/editor-export.ts +103 -0
- package/src/wechat-preview/frontmatter-handler.ts +73 -0
- package/src/wechat-preview/index.test.ts +154 -0
- package/src/wechat-preview/index.ts +294 -0
- package/src/wechat-preview/themes.ts +141 -0
- package/src/wechat-preview/wechat-formatter.test.ts +288 -0
- package/src/wechat-preview/wechat-formatter.ts +1506 -0
- package/src/wechat-preview.test.ts +73 -0
- package/src/workflow-materials.test.ts +225 -0
- package/src/workflow-materials.ts +217 -0
- package/src/workflow.test.ts +2087 -0
- package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
- package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
- package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
- package/dist/cli.js +0 -432
- package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
- package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
- package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
- package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
- package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
- package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
- package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
- package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
- package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
- package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
- /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
package/src/text.ts
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* text.ts — Deterministic text formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Replaces the LLM-based zzhub-media-format skill.
|
|
5
|
+
* Rules sourced from: md-style.md + state-contract.md
|
|
6
|
+
*
|
|
7
|
+
* All functions operate on string buffers — they never mutate files directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Frontmatter ───────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Strip YAML frontmatter from markdown content.
|
|
14
|
+
* Only triggers when content starts with "---\n".
|
|
15
|
+
* Returns clean body (content after second "---\n").
|
|
16
|
+
* If no closing "---" found, returns original content (no-op).
|
|
17
|
+
*/
|
|
18
|
+
export function stripFrontmatter(content: string): string {
|
|
19
|
+
if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
// Find the second "---"
|
|
23
|
+
const lineBreak = content.includes("\r\n") ? "\r\n" : "\n";
|
|
24
|
+
const endMarker = `${lineBreak}---${lineBreak}`;
|
|
25
|
+
const endIdx = content.indexOf(endMarker, 4);
|
|
26
|
+
if (endIdx === -1) {
|
|
27
|
+
// Try end-of-file variant: "---" at the end without trailing newline
|
|
28
|
+
const endMarkerEof = `${lineBreak}---`;
|
|
29
|
+
if (content.endsWith(endMarkerEof)) {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
return content.slice(endIdx + endMarker.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract frontmatter as raw string (without delimiters).
|
|
39
|
+
* Returns null if no frontmatter found.
|
|
40
|
+
*/
|
|
41
|
+
export function extractFrontmatter(content: string): string | null {
|
|
42
|
+
if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const lineBreak = content.includes("\r\n") ? "\r\n" : "\n";
|
|
46
|
+
const startLen = 3 + lineBreak.length; // "---\n"
|
|
47
|
+
const endMarker = `${lineBreak}---${lineBreak}`;
|
|
48
|
+
const endIdx = content.indexOf(endMarker, startLen);
|
|
49
|
+
if (endIdx === -1) {
|
|
50
|
+
const endMarkerEof = `${lineBreak}---`;
|
|
51
|
+
if (content.endsWith(endMarkerEof)) {
|
|
52
|
+
return content.slice(startLen, content.length - endMarkerEof.length);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return content.slice(startLen, endIdx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── CJK spacing ───────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Insert a space between CJK characters and ASCII letters/digits.
|
|
63
|
+
* e.g. "使用OpenClaw管理" -> "使用 OpenClaw 管理"
|
|
64
|
+
*/
|
|
65
|
+
export function fixCjkSpacing(text: string): string {
|
|
66
|
+
// CJK Unified Ideographs + common CJK ranges
|
|
67
|
+
const cjk =
|
|
68
|
+
/[\u2E80-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF01-\uFF60]/;
|
|
69
|
+
|
|
70
|
+
const chars = [...text];
|
|
71
|
+
const result: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < chars.length; i++) {
|
|
74
|
+
result.push(chars[i]);
|
|
75
|
+
|
|
76
|
+
if (i < chars.length - 1) {
|
|
77
|
+
const curr = chars[i];
|
|
78
|
+
const next = chars[i + 1];
|
|
79
|
+
const currIsCjk = cjk.test(curr);
|
|
80
|
+
const nextIsCjk = cjk.test(next);
|
|
81
|
+
const currIsAsciiWord = /[A-Za-z0-9]/.test(curr);
|
|
82
|
+
const nextIsAsciiWord = /[A-Za-z0-9]/.test(next);
|
|
83
|
+
|
|
84
|
+
// CJK followed by ASCII word char, or ASCII word char followed by CJK
|
|
85
|
+
if (
|
|
86
|
+
(currIsCjk && nextIsAsciiWord) ||
|
|
87
|
+
(currIsAsciiWord && nextIsCjk)
|
|
88
|
+
) {
|
|
89
|
+
result.push(" ");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result.join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Heading normalization ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Downgrade H1 headings to H2 in article/blog content.
|
|
101
|
+
* Rule: "公众号正文不用一级标题"
|
|
102
|
+
*/
|
|
103
|
+
export function downgradeH1(text: string): string {
|
|
104
|
+
return text.replace(/^# (?!#)/gm, "## ");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove a leading H1 block from markdown content.
|
|
109
|
+
* Used for channels where the rendered title is provided out-of-band.
|
|
110
|
+
*/
|
|
111
|
+
export function stripLeadingH1(text: string): string {
|
|
112
|
+
const lineBreak = text.includes("\r\n") ? "\r\n" : "\n";
|
|
113
|
+
const lines = text.split(/\r?\n/);
|
|
114
|
+
let firstContentIndex = -1;
|
|
115
|
+
|
|
116
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
117
|
+
if (lines[index].trim()) {
|
|
118
|
+
firstContentIndex = index;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (firstContentIndex === -1 || !/^# (?!#)/.test(lines[firstContentIndex].trim())) {
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let nextIndex = firstContentIndex + 1;
|
|
128
|
+
while (nextIndex < lines.length && !lines[nextIndex].trim()) {
|
|
129
|
+
nextIndex += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [...lines.slice(0, firstContentIndex), ...lines.slice(nextIndex)].join(lineBreak);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeHeadingText(value: string): string {
|
|
136
|
+
return value.trim().replace(/\s+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Remove the leading title heading block when it duplicates metadata.title.
|
|
141
|
+
* Accepts either H1 or H2 because prepare formatting may already downgrade H1.
|
|
142
|
+
*/
|
|
143
|
+
export function stripLeadingTitleHeading(text: string, title: string): string {
|
|
144
|
+
const normalizedTitle = normalizeHeadingText(title);
|
|
145
|
+
if (!normalizedTitle) {
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lineBreak = text.includes("\r\n") ? "\r\n" : "\n";
|
|
150
|
+
const lines = text.split(/\r?\n/);
|
|
151
|
+
let firstContentIndex = -1;
|
|
152
|
+
|
|
153
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
154
|
+
if (lines[index].trim()) {
|
|
155
|
+
firstContentIndex = index;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (firstContentIndex === -1) {
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const match = lines[firstContentIndex].trim().match(/^##?\s+(.+)$/);
|
|
165
|
+
if (!match || normalizeHeadingText(match[1]) !== normalizedTitle) {
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let nextIndex = firstContentIndex + 1;
|
|
170
|
+
while (nextIndex < lines.length && !lines[nextIndex].trim()) {
|
|
171
|
+
nextIndex += 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return [...lines.slice(0, firstContentIndex), ...lines.slice(nextIndex)].join(lineBreak);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove numeric prefixes from H2 headings.
|
|
179
|
+
* e.g. "## 1. 标题" -> "## 标题", "## 1、标题" -> "## 标题"
|
|
180
|
+
*/
|
|
181
|
+
export function removeHeadingNumbers(text: string): string {
|
|
182
|
+
return text.replace(
|
|
183
|
+
/^(##\s+)\d+[.、.]\s*/gm,
|
|
184
|
+
"$1",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Separator normalization ───────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Replace horizontal rules (---, ***, ___) with blank lines.
|
|
192
|
+
* Rule: "不用 --- 分割线语法,用空行或段落自然分隔"
|
|
193
|
+
*/
|
|
194
|
+
export function removeHorizontalRules(text: string): string {
|
|
195
|
+
return text.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove newspic pagination markers such as 【第一页】【第二页】【第N页】
|
|
200
|
+
* and their English equivalents like 【Page 1】.
|
|
201
|
+
* These are editor/orchestrator convenience markers and must not appear in final output.
|
|
202
|
+
*/
|
|
203
|
+
export function removePageMarkers(text: string): string {
|
|
204
|
+
// Matches Chinese ordinals, digits, and the literal placeholder N used in
|
|
205
|
+
// editor/orchestrator prompts such as 【第N页】. Also supports English
|
|
206
|
+
// fallbacks like 【Page 1】 / 【Page N】.
|
|
207
|
+
return text.replace(
|
|
208
|
+
/【(第(?:[一二三四五六七八九十百\d]+|[Nn])页|Page\s*(?:\d+|[Nn]))】\s*/g,
|
|
209
|
+
"",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Blank line compression ────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Compress consecutive blank lines to at most `max` blank lines.
|
|
217
|
+
* Default: 2 (matching state-contract.md rule).
|
|
218
|
+
*/
|
|
219
|
+
export function compressBlankLines(text: string, max = 2): string {
|
|
220
|
+
const threshold = max + 1; // max blank lines = max empty lines between content
|
|
221
|
+
const pattern = new RegExp(`(\\n){${threshold + 1},}`, "g");
|
|
222
|
+
return text.replace(pattern, "\n".repeat(threshold));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function splitMarkdownTableRow(line: string): string[] {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
const normalized = trimmed.replace(/^\|/, "").replace(/\|$/, "");
|
|
228
|
+
return normalized.split("|").map((cell) => cell.trim());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isMarkdownTableSeparator(line: string): boolean {
|
|
232
|
+
const cells = splitMarkdownTableRow(line);
|
|
233
|
+
return (
|
|
234
|
+
cells.length > 0 &&
|
|
235
|
+
cells.every((cell) => /^:?-{3,}:?$/.test(cell))
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isMarkdownTableRow(line: string): boolean {
|
|
240
|
+
const trimmed = line.trim();
|
|
241
|
+
if (!trimmed.includes("|")) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const cells = splitMarkdownTableRow(trimmed);
|
|
245
|
+
return cells.length >= 2 && cells.some((cell) => cell.length > 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function convertMarkdownTableBlock(lines: string[]): string[] {
|
|
249
|
+
if (lines.length < 3) {
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const headers = splitMarkdownTableRow(lines[0]);
|
|
254
|
+
const separator = lines[1];
|
|
255
|
+
const rows = lines.slice(2).map(splitMarkdownTableRow);
|
|
256
|
+
|
|
257
|
+
if (!isMarkdownTableSeparator(separator) || headers.length < 2) {
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const columnCount = headers.length;
|
|
262
|
+
const normalizedRows = rows
|
|
263
|
+
.map((row) => {
|
|
264
|
+
const cells = [...row];
|
|
265
|
+
while (cells.length < columnCount) cells.push("");
|
|
266
|
+
return cells.slice(0, columnCount);
|
|
267
|
+
})
|
|
268
|
+
.filter((row) => row.some((cell) => cell.trim().length > 0));
|
|
269
|
+
|
|
270
|
+
if (normalizedRows.length === 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const blocks: string[] = [];
|
|
275
|
+
|
|
276
|
+
for (let col = 1; col < columnCount; col += 1) {
|
|
277
|
+
const sectionTitle = headers[col]?.trim();
|
|
278
|
+
if (!sectionTitle) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const bulletLines = normalizedRows
|
|
283
|
+
.map((row) => {
|
|
284
|
+
const label = row[0]?.trim();
|
|
285
|
+
const value = row[col]?.trim();
|
|
286
|
+
|
|
287
|
+
if (!value || value === "-" || value === "/") {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (!label) {
|
|
291
|
+
return `- ${value}`;
|
|
292
|
+
}
|
|
293
|
+
return `- ${label}:${value}`;
|
|
294
|
+
})
|
|
295
|
+
.filter((line): line is string => line !== null);
|
|
296
|
+
|
|
297
|
+
if (bulletLines.length === 0) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
blocks.push(sectionTitle);
|
|
302
|
+
blocks.push(...bulletLines);
|
|
303
|
+
blocks.push("");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
while (blocks.length > 0 && blocks[blocks.length - 1] === "") {
|
|
307
|
+
blocks.pop();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return blocks;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function convertMarkdownTablesToCompareBlocks(content: string): string {
|
|
314
|
+
const lines = content.split(/\r?\n/);
|
|
315
|
+
const output: string[] = [];
|
|
316
|
+
let index = 0;
|
|
317
|
+
|
|
318
|
+
while (index < lines.length) {
|
|
319
|
+
if (
|
|
320
|
+
index + 2 < lines.length &&
|
|
321
|
+
isMarkdownTableRow(lines[index]) &&
|
|
322
|
+
isMarkdownTableSeparator(lines[index + 1])
|
|
323
|
+
) {
|
|
324
|
+
const block = [lines[index], lines[index + 1]];
|
|
325
|
+
let cursor = index + 2;
|
|
326
|
+
while (cursor < lines.length && isMarkdownTableRow(lines[cursor])) {
|
|
327
|
+
block.push(lines[cursor]);
|
|
328
|
+
cursor += 1;
|
|
329
|
+
}
|
|
330
|
+
output.push(...convertMarkdownTableBlock(block));
|
|
331
|
+
index = cursor;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
output.push(lines[index]);
|
|
336
|
+
index += 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return output.join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Illustration markers ──────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Remove illustration markers (插图N / 配图N) from text.
|
|
346
|
+
* Rule from state-contract.md: /[插配]图\d+/g
|
|
347
|
+
* Does not modify original file — operates on buffer.
|
|
348
|
+
*/
|
|
349
|
+
export function removeIllustrationMarkers(text: string): string {
|
|
350
|
+
// Matches standalone 插图1, 配图1, [插图1], [插图 1], etc.
|
|
351
|
+
// Strip the marker and any surrounding whitespace on the same line,
|
|
352
|
+
// so "驻扎,[插图 1] 风" → "驻扎,风" (no stray space left behind)
|
|
353
|
+
// Negative lookbehind: do NOT match when preceded by path chars (/, ., alphanumeric)
|
|
354
|
+
// to avoid eating "插图1" inside image paths like 
|
|
355
|
+
return text.replace(/(?<![/.\w])[ \t]*\[?[插配]图\s*\d+\]?[ \t]*/g, "");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Count illustration markers in text.
|
|
360
|
+
* Returns array of matched markers.
|
|
361
|
+
*/
|
|
362
|
+
export function findIllustrationMarkers(text: string): string[] {
|
|
363
|
+
const matches = text.match(/[插配]图\d+/g);
|
|
364
|
+
return matches ?? [];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function escapeRegExp(value: string): string {
|
|
368
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Replace illustration markers with markdown image blocks.
|
|
373
|
+
* Used by wechat-article publish after the user has supplied body images.
|
|
374
|
+
*/
|
|
375
|
+
export function injectIllustrationImages(
|
|
376
|
+
text: string,
|
|
377
|
+
received: Array<{ marker: string; path: string }>,
|
|
378
|
+
): string {
|
|
379
|
+
let result = text;
|
|
380
|
+
|
|
381
|
+
for (const item of received) {
|
|
382
|
+
if (!item.marker || !item.path) continue;
|
|
383
|
+
const replacement = `\n\n\n\n`;
|
|
384
|
+
result = result.replace(
|
|
385
|
+
new RegExp(escapeRegExp(item.marker), "g"),
|
|
386
|
+
replacement,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return compressBlankLines(result);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Markdown stripping (for word count) ───────────────────────────
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Strip Markdown syntax to get plain text (for word/char counting).
|
|
397
|
+
* Removes: headings, bold, italic, links, images, code fences, inline code, blockquotes.
|
|
398
|
+
*/
|
|
399
|
+
export function stripMarkdown(text: string): string {
|
|
400
|
+
return (
|
|
401
|
+
text
|
|
402
|
+
// Remove code fences
|
|
403
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
404
|
+
// Remove inline code
|
|
405
|
+
.replace(/`[^`]+`/g, "")
|
|
406
|
+
// Remove images
|
|
407
|
+
.replace(/!\[.*?\]\(.*?\)/g, "")
|
|
408
|
+
// Remove links (keep text)
|
|
409
|
+
.replace(/\[([^\]]*)\]\(.*?\)/g, "$1")
|
|
410
|
+
// Remove headings markers
|
|
411
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
412
|
+
// Remove bold/italic markers
|
|
413
|
+
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
|
|
414
|
+
// Remove blockquote markers
|
|
415
|
+
.replace(/^>\s?/gm, "")
|
|
416
|
+
// Remove horizontal rules
|
|
417
|
+
.replace(/^[-*_]{3,}\s*$/gm, "")
|
|
418
|
+
.trim()
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Slug generation ───────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Generate a kebab-case slug from a title.
|
|
426
|
+
* Handles CJK by pinyin-free approach: transliterate common chars, keep ASCII.
|
|
427
|
+
* For CJK-heavy titles, uses a simplified approach.
|
|
428
|
+
*/
|
|
429
|
+
export function generateSlug(title: string): string {
|
|
430
|
+
let slug = title
|
|
431
|
+
.toLowerCase()
|
|
432
|
+
.trim()
|
|
433
|
+
// Replace CJK punctuation with spaces
|
|
434
|
+
.replace(/[,。!?、;:""''【】《》()…—~·]+/g, " ")
|
|
435
|
+
// Replace ASCII punctuation with spaces
|
|
436
|
+
.replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, " ")
|
|
437
|
+
// For CJK chars: just remove them (slug will be ASCII-only)
|
|
438
|
+
.replace(/[\u4e00-\u9fff]+/g, " ")
|
|
439
|
+
// Collapse whitespace
|
|
440
|
+
.replace(/\s+/g, "-")
|
|
441
|
+
// Remove leading/trailing dashes
|
|
442
|
+
.replace(/^-+|-+$/g, "");
|
|
443
|
+
|
|
444
|
+
// If slug is empty (all CJK), fall back to date-based
|
|
445
|
+
if (!slug) {
|
|
446
|
+
const now = new Date();
|
|
447
|
+
slug = `post-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return slug;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Date formatting ───────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get current date as YYYY-MM-DD string.
|
|
457
|
+
*/
|
|
458
|
+
export function todayDate(): string {
|
|
459
|
+
const now = new Date();
|
|
460
|
+
const y = now.getFullYear();
|
|
461
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
462
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
463
|
+
return `${y}-${m}-${d}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Description extraction ────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Extract description from body text.
|
|
470
|
+
* Takes first non-empty, non-heading line, strips markdown.
|
|
471
|
+
* For wechat-article: description must not be null.
|
|
472
|
+
*/
|
|
473
|
+
export function extractDescription(body: string): string {
|
|
474
|
+
const lines = body.split("\n");
|
|
475
|
+
for (const line of lines) {
|
|
476
|
+
const trimmed = line.trim();
|
|
477
|
+
if (!trimmed) continue;
|
|
478
|
+
if (/^#{1,6}\s/.test(trimmed)) continue;
|
|
479
|
+
// Strip inline markdown
|
|
480
|
+
const clean = trimmed
|
|
481
|
+
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
|
|
482
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
483
|
+
.replace(/\[([^\]]*)\]\(.*?\)/g, "$1")
|
|
484
|
+
.trim();
|
|
485
|
+
if (clean) return clean;
|
|
486
|
+
}
|
|
487
|
+
return "";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Cover title generation ────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Generate cover title for poster-3-4 template.
|
|
494
|
+
* Rules from image-plan SKILL.md:
|
|
495
|
+
* 1. Take metadata.title
|
|
496
|
+
* 2. If contains colon, take the part before colon
|
|
497
|
+
* 3. Remove common suffixes (介绍/解读/盘点/更新/亮点/发布/公告)
|
|
498
|
+
* 4. Result must be <= 15 chars
|
|
499
|
+
* 5. If too long, try to shorten; fallback to full title
|
|
500
|
+
*/
|
|
501
|
+
export function generateCoverTitle(title: string): string {
|
|
502
|
+
let result = title;
|
|
503
|
+
|
|
504
|
+
// If contains colon (Chinese or ASCII), take first part
|
|
505
|
+
const colonIdx = result.search(/[::]/);
|
|
506
|
+
if (colonIdx > 0) {
|
|
507
|
+
result = result.slice(0, colonIdx).trim();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Remove common suffixes
|
|
511
|
+
result = result.replace(/[介解盘更亮发公]?(?:绍|读|点|新|点|布|告)$/, "").trim();
|
|
512
|
+
|
|
513
|
+
// Check length
|
|
514
|
+
if ([...result].length <= 15) {
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Fallback: return full title (let imgx handle truncation)
|
|
519
|
+
return title;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── Highlight words extraction ────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Extract 1-3 highlight words from a title.
|
|
526
|
+
* Simple algorithm: regex tokenize, prefer shorter words.
|
|
527
|
+
* Rules from publish-rules.md.
|
|
528
|
+
*/
|
|
529
|
+
export function extractHighlightWords(title: string): string[] {
|
|
530
|
+
// Tokenize: split on spaces, punctuation, CJK boundaries
|
|
531
|
+
const tokens: string[] = [];
|
|
532
|
+
|
|
533
|
+
// Extract English words
|
|
534
|
+
const englishWords = title.match(/[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)*/g);
|
|
535
|
+
if (englishWords) {
|
|
536
|
+
tokens.push(...englishWords);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (tokens.length === 0) return [];
|
|
540
|
+
|
|
541
|
+
// Deduplicate (case-insensitive)
|
|
542
|
+
const seen = new Set<string>();
|
|
543
|
+
const unique: string[] = [];
|
|
544
|
+
for (const t of tokens) {
|
|
545
|
+
const key = t.toLowerCase();
|
|
546
|
+
if (!seen.has(key)) {
|
|
547
|
+
seen.add(key);
|
|
548
|
+
unique.push(t);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Filter common stopwords
|
|
553
|
+
const stopWords = new Set([
|
|
554
|
+
"the", "and", "for", "with", "from", "this", "that",
|
|
555
|
+
"have", "been", "will", "your", "what", "how", "why",
|
|
556
|
+
"can", "not", "are", "was", "were", "has", "had",
|
|
557
|
+
"its", "his", "her", "our", "their", "you", "all",
|
|
558
|
+
"but", "also", "just", "only", "very", "too", "over",
|
|
559
|
+
"into", "more", "some", "such", "than", "then", "when",
|
|
560
|
+
]);
|
|
561
|
+
|
|
562
|
+
const filtered = unique.filter((w) => !stopWords.has(w.toLowerCase()));
|
|
563
|
+
|
|
564
|
+
// Minimum length filter
|
|
565
|
+
const minLengthFiltered = filtered.filter((w) => {
|
|
566
|
+
// Single-character words are never meaningful highlights
|
|
567
|
+
if (w.length < 2) return false;
|
|
568
|
+
// Lowercase-only words must be at least 3 chars (skip prepositions like "to", "in")
|
|
569
|
+
if (w === w.toLowerCase() && w.length < 3) return false;
|
|
570
|
+
return true;
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (minLengthFiltered.length === 0) return [];
|
|
574
|
+
|
|
575
|
+
// Sort: shorter words first (more impactful as highlights)
|
|
576
|
+
minLengthFiltered.sort((a, b) => [...a].length - [...b].length);
|
|
577
|
+
|
|
578
|
+
// Take up to 3
|
|
579
|
+
return minLengthFiltered.slice(0, 3);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── Full format pipeline ──────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Apply all formatting rules to article/blog markdown content.
|
|
586
|
+
* This is the deterministic replacement for the format skill.
|
|
587
|
+
*
|
|
588
|
+
* Steps:
|
|
589
|
+
* 1. Fix CJK-ASCII spacing
|
|
590
|
+
* 2. Downgrade H1 to H2
|
|
591
|
+
* 3. Remove heading number prefixes
|
|
592
|
+
* 4. Replace horizontal rules with blank lines
|
|
593
|
+
* 5. Strip newspic pagination markers (【第N页】)
|
|
594
|
+
* 6. Strip illustration markers ([插图1], [插图 1], 插图1, etc.)
|
|
595
|
+
* 7. Compress consecutive blank lines (max 2)
|
|
596
|
+
*/
|
|
597
|
+
export function formatArticle(body: string): string {
|
|
598
|
+
let result = body;
|
|
599
|
+
result = fixCjkSpacing(result);
|
|
600
|
+
result = downgradeH1(result);
|
|
601
|
+
result = removeHeadingNumbers(result);
|
|
602
|
+
result = removeHorizontalRules(result);
|
|
603
|
+
result = removePageMarkers(result);
|
|
604
|
+
result = removeIllustrationMarkers(result);
|
|
605
|
+
result = compressBlankLines(result);
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Prepare body text for imgx consumption:
|
|
611
|
+
* 1. Strip frontmatter
|
|
612
|
+
* 2. Remove illustration markers
|
|
613
|
+
* 3. Compress blank lines
|
|
614
|
+
*/
|
|
615
|
+
export function prepareBodyForImgx(content: string): string {
|
|
616
|
+
let result = stripFrontmatter(content);
|
|
617
|
+
result = removeIllustrationMarkers(result);
|
|
618
|
+
result = compressBlankLines(result);
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Prepare body for newspic publish (post-clean.md):
|
|
624
|
+
* 1. Strip frontmatter
|
|
625
|
+
* 2. Strip markdown decorations (but keep structure)
|
|
626
|
+
* 3. Strip newspic pagination markers such as 【第N页】
|
|
627
|
+
*/
|
|
628
|
+
export function prepareBodyForNewspic(content: string): string {
|
|
629
|
+
let result = stripFrontmatter(content);
|
|
630
|
+
result = convertMarkdownTablesToCompareBlocks(result);
|
|
631
|
+
// Remove code fences first (before inline code regex can eat backticks)
|
|
632
|
+
result = result.replace(/```[\s\S]*?```/g, "");
|
|
633
|
+
// Remove bold/italic markers
|
|
634
|
+
result = result.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1");
|
|
635
|
+
// Remove inline code (keep text)
|
|
636
|
+
result = result.replace(/`([^`]+)`/g, "$1");
|
|
637
|
+
// Remove images first (before link regex can partially match 
|
|
638
|
+
result = result.replace(/!\[.*?\]\(.*?\)/g, "");
|
|
639
|
+
// Remove links (keep text)
|
|
640
|
+
result = result.replace(/\[([^\]]*)\]\(.*?\)/g, "$1");
|
|
641
|
+
// Remove heading markers (keep text)
|
|
642
|
+
result = result.replace(/^#{1,6}\s+/gm, "");
|
|
643
|
+
// Remove blockquote markers (keep text)
|
|
644
|
+
result = result.replace(/^>\s?/gm, "");
|
|
645
|
+
// Remove horizontal rules
|
|
646
|
+
result = result.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
647
|
+
// Remove newspic page markers from final publish content
|
|
648
|
+
result = removePageMarkers(result);
|
|
649
|
+
// Remove illustration markers ([插图1], [插图 1], 插图1, etc.)
|
|
650
|
+
result = removeIllustrationMarkers(result);
|
|
651
|
+
// Compress blank lines
|
|
652
|
+
result = compressBlankLines(result);
|
|
653
|
+
return result.trim() + "\n";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── Frontmatter builder ───────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Build YAML frontmatter string from metadata + route.
|
|
660
|
+
*/
|
|
661
|
+
export function buildFrontmatter(meta: {
|
|
662
|
+
title: string;
|
|
663
|
+
date: string;
|
|
664
|
+
platform: string;
|
|
665
|
+
description?: string | null;
|
|
666
|
+
tags?: string[];
|
|
667
|
+
}): string {
|
|
668
|
+
const lines = ["---"];
|
|
669
|
+
lines.push(`title: "${meta.title.replace(/"/g, '\\"')}"`);
|
|
670
|
+
lines.push(`date: ${meta.date}`);
|
|
671
|
+
lines.push(`platform: ${meta.platform}`);
|
|
672
|
+
if (meta.description) {
|
|
673
|
+
lines.push(`description: "${meta.description.replace(/"/g, '\\"')}"`);
|
|
674
|
+
}
|
|
675
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
676
|
+
lines.push(`tags:`);
|
|
677
|
+
for (const tag of meta.tags) {
|
|
678
|
+
lines.push(` - ${tag}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
lines.push("---");
|
|
682
|
+
return lines.join("\n");
|
|
683
|
+
}
|
|
Binary file
|