@zzclub/pipeline 0.3.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 -431
- 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.test.ts
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { describe, test, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
stripFrontmatter,
|
|
4
|
+
extractFrontmatter,
|
|
5
|
+
fixCjkSpacing,
|
|
6
|
+
downgradeH1,
|
|
7
|
+
removeHeadingNumbers,
|
|
8
|
+
removeHorizontalRules,
|
|
9
|
+
compressBlankLines,
|
|
10
|
+
removeIllustrationMarkers,
|
|
11
|
+
findIllustrationMarkers,
|
|
12
|
+
injectIllustrationImages,
|
|
13
|
+
stripMarkdown,
|
|
14
|
+
generateSlug,
|
|
15
|
+
extractDescription,
|
|
16
|
+
generateCoverTitle,
|
|
17
|
+
extractHighlightWords,
|
|
18
|
+
formatArticle,
|
|
19
|
+
prepareBodyForImgx,
|
|
20
|
+
prepareBodyForNewspic,
|
|
21
|
+
buildFrontmatter,
|
|
22
|
+
} from "./text";
|
|
23
|
+
|
|
24
|
+
// ── stripFrontmatter ──────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("stripFrontmatter", () => {
|
|
27
|
+
test("strips valid frontmatter", () => {
|
|
28
|
+
const input = "---\ntitle: Hello\n---\nBody text here";
|
|
29
|
+
expect(stripFrontmatter(input)).toBe("Body text here");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns content as-is when no frontmatter", () => {
|
|
33
|
+
const input = "Just body text\nNo frontmatter";
|
|
34
|
+
expect(stripFrontmatter(input)).toBe(input);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("handles frontmatter with CRLF", () => {
|
|
38
|
+
const input = "---\r\ntitle: Hello\r\n---\r\nBody text";
|
|
39
|
+
expect(stripFrontmatter(input)).toBe("Body text");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("handles frontmatter at end of file", () => {
|
|
43
|
+
const input = "---\ntitle: Hello\n---";
|
|
44
|
+
expect(stripFrontmatter(input)).toBe("");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("handles no closing delimiter", () => {
|
|
48
|
+
const input = "---\ntitle: Hello\nno closing";
|
|
49
|
+
expect(stripFrontmatter(input)).toBe(input);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("handles empty body after frontmatter", () => {
|
|
53
|
+
const input = "---\ntitle: Hello\n---\n";
|
|
54
|
+
expect(stripFrontmatter(input)).toBe("");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("preserves content that starts with --- inside text", () => {
|
|
58
|
+
const input = "Some text\n---\nMore text";
|
|
59
|
+
expect(stripFrontmatter(input)).toBe(input);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── extractFrontmatter ────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("extractFrontmatter", () => {
|
|
66
|
+
test("extracts frontmatter content", () => {
|
|
67
|
+
const input = "---\ntitle: Hello\ndate: 2025-01-01\n---\nBody";
|
|
68
|
+
expect(extractFrontmatter(input)).toBe("title: Hello\ndate: 2025-01-01");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns null when no frontmatter", () => {
|
|
72
|
+
expect(extractFrontmatter("No frontmatter here")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns null when no closing delimiter", () => {
|
|
76
|
+
const input = "---\ntitle: Hello\nno closing";
|
|
77
|
+
expect(extractFrontmatter(input)).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("extracts frontmatter at end of file", () => {
|
|
81
|
+
const input = "---\ntitle: Hello\n---";
|
|
82
|
+
expect(extractFrontmatter(input)).toBe("title: Hello");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── fixCjkSpacing ─────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe("fixCjkSpacing", () => {
|
|
89
|
+
test("adds space between CJK and ASCII", () => {
|
|
90
|
+
expect(fixCjkSpacing("使用OpenClaw管理")).toBe("使用 OpenClaw 管理");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("adds space between ASCII digit and CJK", () => {
|
|
94
|
+
expect(fixCjkSpacing("版本3发布")).toBe("版本 3 发布");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("does not double-space if already spaced", () => {
|
|
98
|
+
expect(fixCjkSpacing("使用 OpenClaw 管理")).toBe("使用 OpenClaw 管理");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("handles pure ASCII text", () => {
|
|
102
|
+
expect(fixCjkSpacing("Hello World")).toBe("Hello World");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("handles pure CJK text", () => {
|
|
106
|
+
expect(fixCjkSpacing("你好世界")).toBe("你好世界");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("handles mixed CJK and numbers", () => {
|
|
110
|
+
expect(fixCjkSpacing("第1章")).toBe("第 1 章");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("handles empty string", () => {
|
|
114
|
+
expect(fixCjkSpacing("")).toBe("");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("handles Japanese kana", () => {
|
|
118
|
+
expect(fixCjkSpacing("テストtest")).toBe("テスト test");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── downgradeH1 ───────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe("downgradeH1", () => {
|
|
125
|
+
test("converts H1 to H2", () => {
|
|
126
|
+
expect(downgradeH1("# Title")).toBe("## Title");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("does not affect H2+", () => {
|
|
130
|
+
expect(downgradeH1("## Subtitle")).toBe("## Subtitle");
|
|
131
|
+
expect(downgradeH1("### H3")).toBe("### H3");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("handles multiple H1s", () => {
|
|
135
|
+
const input = "# First\nSome text\n# Second";
|
|
136
|
+
expect(downgradeH1(input)).toBe("## First\nSome text\n## Second");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("does not affect inline #", () => {
|
|
140
|
+
const input = "Use #hashtag in text";
|
|
141
|
+
expect(downgradeH1(input)).toBe("Use #hashtag in text");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── removeHeadingNumbers ──────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
describe("removeHeadingNumbers", () => {
|
|
148
|
+
test("removes numeric prefix with dot", () => {
|
|
149
|
+
expect(removeHeadingNumbers("## 1. 标题")).toBe("## 标题");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("removes numeric prefix with Chinese comma", () => {
|
|
153
|
+
expect(removeHeadingNumbers("## 1、标题")).toBe("## 标题");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("removes numeric prefix with full-width dot", () => {
|
|
157
|
+
expect(removeHeadingNumbers("## 1.标题")).toBe("## 标题");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("handles multi-digit numbers", () => {
|
|
161
|
+
expect(removeHeadingNumbers("## 12. 标题")).toBe("## 标题");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("does not affect H1 or H3", () => {
|
|
165
|
+
expect(removeHeadingNumbers("# 1. Title")).toBe("# 1. Title");
|
|
166
|
+
expect(removeHeadingNumbers("### 1. Title")).toBe("### 1. Title");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("does not affect headings without numbers", () => {
|
|
170
|
+
expect(removeHeadingNumbers("## 标题")).toBe("## 标题");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── removeHorizontalRules ─────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
describe("removeHorizontalRules", () => {
|
|
177
|
+
test("removes ---", () => {
|
|
178
|
+
expect(removeHorizontalRules("above\n---\nbelow")).toBe(
|
|
179
|
+
"above\n\nbelow",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("removes ***", () => {
|
|
184
|
+
expect(removeHorizontalRules("above\n***\nbelow")).toBe(
|
|
185
|
+
"above\n\nbelow",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("removes ___", () => {
|
|
190
|
+
expect(removeHorizontalRules("above\n___\nbelow")).toBe(
|
|
191
|
+
"above\n\nbelow",
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("removes long variants", () => {
|
|
196
|
+
expect(removeHorizontalRules("above\n-----\nbelow")).toBe(
|
|
197
|
+
"above\n\nbelow",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("does not remove inline dashes", () => {
|
|
202
|
+
expect(removeHorizontalRules("word--word")).toBe("word--word");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── compressBlankLines ────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
describe("compressBlankLines", () => {
|
|
209
|
+
test("compresses 4+ blank lines to 2", () => {
|
|
210
|
+
const input = "A\n\n\n\n\nB";
|
|
211
|
+
const result = compressBlankLines(input);
|
|
212
|
+
expect(result).toBe("A\n\n\nB");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("leaves 2 blank lines as-is", () => {
|
|
216
|
+
const input = "A\n\n\nB";
|
|
217
|
+
expect(compressBlankLines(input)).toBe("A\n\n\nB");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("leaves single blank line as-is", () => {
|
|
221
|
+
const input = "A\n\nB";
|
|
222
|
+
expect(compressBlankLines(input)).toBe("A\n\nB");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("respects custom max parameter", () => {
|
|
226
|
+
const input = "A\n\n\n\nB";
|
|
227
|
+
expect(compressBlankLines(input, 1)).toBe("A\n\nB");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── removeIllustrationMarkers / findIllustrationMarkers ───────────
|
|
232
|
+
|
|
233
|
+
describe("removeIllustrationMarkers", () => {
|
|
234
|
+
test("removes 插图 markers", () => {
|
|
235
|
+
expect(removeIllustrationMarkers("上面是插图1的内容")).toBe(
|
|
236
|
+
"上面是的内容",
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("removes 配图 markers", () => {
|
|
241
|
+
expect(removeIllustrationMarkers("这是配图2")).toBe("这是");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("removes multiple markers", () => {
|
|
245
|
+
expect(removeIllustrationMarkers("插图1和插图2")).toBe("和");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("no-op on text without markers", () => {
|
|
249
|
+
const input = "Regular text";
|
|
250
|
+
expect(removeIllustrationMarkers(input)).toBe(input);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("does NOT strip 插图 inside image paths", () => {
|
|
254
|
+
const input = "";
|
|
255
|
+
expect(removeIllustrationMarkers(input)).toBe(input);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("does NOT strip 插图 inside markdown image alt/path", () => {
|
|
259
|
+
const input = "";
|
|
260
|
+
expect(removeIllustrationMarkers(input)).toBe(input);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("still removes standalone marker after image", () => {
|
|
264
|
+
const input = "\n\n插图3\n\n后续文字";
|
|
265
|
+
expect(removeIllustrationMarkers(input)).toBe("\n\n\n\n后续文字");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("findIllustrationMarkers", () => {
|
|
270
|
+
test("finds markers", () => {
|
|
271
|
+
expect(findIllustrationMarkers("插图1和配图2")).toEqual(["插图1", "配图2"]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("returns empty array when none found", () => {
|
|
275
|
+
expect(findIllustrationMarkers("no markers")).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("injectIllustrationImages", () => {
|
|
280
|
+
test("replaces markers with markdown image blocks", () => {
|
|
281
|
+
const result = injectIllustrationImages("开头\n\n插图1\n\n结尾", [
|
|
282
|
+
{ marker: "插图1", path: "/tmp/body-image.png" },
|
|
283
|
+
]);
|
|
284
|
+
expect(result).toContain("");
|
|
285
|
+
expect(result).not.toContain("插图1");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("leaves unrelated content untouched", () => {
|
|
289
|
+
const input = "没有标记的正文";
|
|
290
|
+
expect(injectIllustrationImages(input, [
|
|
291
|
+
{ marker: "插图1", path: "/tmp/body-image.png" },
|
|
292
|
+
])).toBe(input);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ── stripMarkdown ─────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
describe("stripMarkdown", () => {
|
|
299
|
+
test("strips headings", () => {
|
|
300
|
+
expect(stripMarkdown("## Title\nBody")).toBe("Title\nBody");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("strips bold and italic", () => {
|
|
304
|
+
expect(stripMarkdown("**bold** and *italic*")).toBe("bold and italic");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("strips links (keeps text)", () => {
|
|
308
|
+
expect(stripMarkdown("[link](https://example.com)")).toBe("link");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("strips images", () => {
|
|
312
|
+
expect(stripMarkdown("")).toBe("");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("strips inline code", () => {
|
|
316
|
+
expect(stripMarkdown("Use `code` here")).toBe("Use here");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("strips code fences", () => {
|
|
320
|
+
expect(stripMarkdown("Before\n```js\ncode\n```\nAfter")).toBe(
|
|
321
|
+
"Before\n\nAfter",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("strips blockquotes", () => {
|
|
326
|
+
expect(stripMarkdown("> Quote")).toBe("Quote");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ── generateSlug ──────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
describe("generateSlug", () => {
|
|
333
|
+
test("generates slug from English title", () => {
|
|
334
|
+
expect(generateSlug("Hello World")).toBe("hello-world");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("handles mixed CJK and English", () => {
|
|
338
|
+
const slug = generateSlug("Hello World 管理工具介绍");
|
|
339
|
+
expect(slug).toBe("hello-world");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("removes punctuation", () => {
|
|
343
|
+
expect(generateSlug("Hello, World!")).toBe("hello-world");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("falls back to date for pure CJK", () => {
|
|
347
|
+
const slug = generateSlug("你好世界");
|
|
348
|
+
expect(slug).toMatch(/^post-\d{8}$/);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("handles empty string", () => {
|
|
352
|
+
const slug = generateSlug("");
|
|
353
|
+
expect(slug).toMatch(/^post-\d{8}$/);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("no leading/trailing dashes", () => {
|
|
357
|
+
expect(generateSlug(" Hello ")).toBe("hello");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ── extractDescription ────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
describe("extractDescription", () => {
|
|
364
|
+
test("extracts first non-heading line", () => {
|
|
365
|
+
const input = "## Title\nThis is the description.\nMore text.";
|
|
366
|
+
expect(extractDescription(input)).toBe("This is the description.");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("skips empty lines", () => {
|
|
370
|
+
const input = "\n\n## Title\n\nFirst paragraph.";
|
|
371
|
+
expect(extractDescription(input)).toBe("First paragraph.");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("strips inline markdown", () => {
|
|
375
|
+
const input = "**Bold** description with `code`";
|
|
376
|
+
expect(extractDescription(input)).toBe("Bold description with code");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("returns empty for heading-only content", () => {
|
|
380
|
+
const input = "## Title\n## Another Title";
|
|
381
|
+
expect(extractDescription(input)).toBe("");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("strips links", () => {
|
|
385
|
+
const input = "Check [this](https://example.com) out";
|
|
386
|
+
expect(extractDescription(input)).toBe("Check this out");
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ── generateCoverTitle ────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
describe("generateCoverTitle", () => {
|
|
393
|
+
test("returns short title as-is", () => {
|
|
394
|
+
expect(generateCoverTitle("短标题")).toBe("短标题");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("takes part before colon", () => {
|
|
398
|
+
expect(generateCoverTitle("主标题:副标题")).toBe("主标题");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("takes part before ASCII colon", () => {
|
|
402
|
+
expect(generateCoverTitle("Main: Subtitle")).toBe("Main");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("removes common suffixes", () => {
|
|
406
|
+
expect(generateCoverTitle("功能发布")).toBe("功能");
|
|
407
|
+
expect(generateCoverTitle("版本更新")).toBe("版本");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("returns full title if result > 15 chars", () => {
|
|
411
|
+
const longTitle =
|
|
412
|
+
"这是一个非常非常非常非常非常非常长的标题";
|
|
413
|
+
expect(generateCoverTitle(longTitle)).toBe(longTitle);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ── extractHighlightWords ─────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
describe("extractHighlightWords", () => {
|
|
420
|
+
it("extracts English acronyms like AI, API, GPU", () => {
|
|
421
|
+
expect(extractHighlightWords("AI Agent 时代的 API 设计")).toContain("AI");
|
|
422
|
+
expect(extractHighlightWords("AI Agent 时代的 API 设计")).toContain("API");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("extracts camelCase and capitalized English terms", () => {
|
|
426
|
+
expect(extractHighlightWords("OpenClaw 集群管理实战")).toContain("OpenClaw");
|
|
427
|
+
expect(extractHighlightWords("ChatGPT 与 Agent 开发")).toContain("Agent");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("does NOT extract CJK fragments", () => {
|
|
431
|
+
const result = extractHighlightWords("深度学习框架对比指南");
|
|
432
|
+
for (const word of result) {
|
|
433
|
+
expect(/^[A-Za-z]/.test(word)).toBe(true);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("filters common English stopwords", () => {
|
|
438
|
+
const result = extractHighlightWords("the and for with this that have");
|
|
439
|
+
expect(result.length).toBe(0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("returns empty array for pure CJK title with no English terms", () => {
|
|
443
|
+
expect(extractHighlightWords("深度学习框架对比指南")).toEqual([]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("returns empty array for empty title", () => {
|
|
447
|
+
expect(extractHighlightWords("")).toEqual([]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("returns at most 3 words, sorted shortest first", () => {
|
|
451
|
+
const result = extractHighlightWords("AI Agent Framework OpenClaw API Gateway");
|
|
452
|
+
expect(result.length).toBeLessThanOrEqual(3);
|
|
453
|
+
for (let i = 1; i < result.length; i++) {
|
|
454
|
+
expect(result[i - 1].length).toBeLessThanOrEqual(result[i].length);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("excludes short prepositions and single letters (min length rules)", () => {
|
|
459
|
+
const result = extractHighlightWords("How to use AI in your work at A B C");
|
|
460
|
+
expect(result).not.toContain("to");
|
|
461
|
+
expect(result).not.toContain("in");
|
|
462
|
+
expect(result).not.toContain("A");
|
|
463
|
+
expect(result).not.toContain("B");
|
|
464
|
+
expect(result).toContain("AI");
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ── formatArticle ─────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
describe("formatArticle", () => {
|
|
471
|
+
test("applies all formatting rules in sequence", () => {
|
|
472
|
+
const input = "# 1. 使用OpenClaw的方法\n---\nBody text\n\n\n\n\nMore text";
|
|
473
|
+
const result = formatArticle(input);
|
|
474
|
+
// H1 -> H2
|
|
475
|
+
expect(result).toContain("##");
|
|
476
|
+
expect(result).not.toMatch(/^# /m);
|
|
477
|
+
// CJK spacing
|
|
478
|
+
expect(result).toContain("使用 OpenClaw 的方法");
|
|
479
|
+
// No heading numbers
|
|
480
|
+
expect(result).not.toMatch(/## \d+\./);
|
|
481
|
+
// No horizontal rules
|
|
482
|
+
expect(result).not.toMatch(/^---$/m);
|
|
483
|
+
// Compressed blank lines
|
|
484
|
+
expect(result).not.toContain("\n\n\n\n");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("is idempotent", () => {
|
|
488
|
+
const input = "## 标题\n\n正文内容\n\n更多内容";
|
|
489
|
+
expect(formatArticle(formatArticle(input))).toBe(formatArticle(input));
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// ── prepareBodyForImgx ────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
describe("prepareBodyForImgx", () => {
|
|
496
|
+
test("strips frontmatter and illustration markers", () => {
|
|
497
|
+
const input = "---\ntitle: Hello\n---\n正文插图1更多内容";
|
|
498
|
+
const result = prepareBodyForImgx(input);
|
|
499
|
+
expect(result).not.toContain("---");
|
|
500
|
+
expect(result).not.toContain("插图1");
|
|
501
|
+
expect(result).toContain("正文更多内容");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("compresses blank lines", () => {
|
|
505
|
+
const input = "A\n\n\n\n\nB";
|
|
506
|
+
const result = prepareBodyForImgx(input);
|
|
507
|
+
expect(result).toBe("A\n\n\nB");
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ── prepareBodyForNewspic ─────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
describe("prepareBodyForNewspic", () => {
|
|
514
|
+
test("strips frontmatter", () => {
|
|
515
|
+
const input = "---\ntitle: Hello\n---\nBody text";
|
|
516
|
+
const result = prepareBodyForNewspic(input);
|
|
517
|
+
expect(result).not.toContain("---");
|
|
518
|
+
expect(result).toContain("Body text");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("strips bold/italic/links/images/headings/blockquotes/code", () => {
|
|
522
|
+
const input =
|
|
523
|
+
"**Bold** *italic* [link](url) \n## Heading\n> Quote\n`code`\n```\nblock\n```";
|
|
524
|
+
const result = prepareBodyForNewspic(input);
|
|
525
|
+
expect(result).not.toContain("**");
|
|
526
|
+
expect(result).not.toContain("*");
|
|
527
|
+
expect(result).not.toContain("[link]");
|
|
528
|
+
expect(result).not.toContain("![");
|
|
529
|
+
expect(result).not.toContain("##");
|
|
530
|
+
expect(result).not.toContain(">");
|
|
531
|
+
expect(result).not.toContain("`");
|
|
532
|
+
expect(result).not.toContain("```");
|
|
533
|
+
expect(result).toContain("Bold");
|
|
534
|
+
expect(result).toContain("italic");
|
|
535
|
+
expect(result).toContain("link");
|
|
536
|
+
expect(result).toContain("Heading");
|
|
537
|
+
expect(result).toContain("Quote");
|
|
538
|
+
expect(result).toContain("code");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("ends with single newline", () => {
|
|
542
|
+
const input = "Body text";
|
|
543
|
+
expect(prepareBodyForNewspic(input).endsWith("\n")).toBe(true);
|
|
544
|
+
expect(prepareBodyForNewspic(input).endsWith("\n\n")).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("removes newspic page markers from final publish content", () => {
|
|
548
|
+
const input = [
|
|
549
|
+
"【第N页】",
|
|
550
|
+
"占位分页内容",
|
|
551
|
+
"",
|
|
552
|
+
"【第1页】",
|
|
553
|
+
"第一页内容",
|
|
554
|
+
"",
|
|
555
|
+
"【第2页】",
|
|
556
|
+
"第二页内容",
|
|
557
|
+
"",
|
|
558
|
+
"【Page 3】",
|
|
559
|
+
"Third page",
|
|
560
|
+
].join("\n");
|
|
561
|
+
|
|
562
|
+
expect(prepareBodyForNewspic(input)).toBe(
|
|
563
|
+
[
|
|
564
|
+
"占位分页内容",
|
|
565
|
+
"",
|
|
566
|
+
"第一页内容",
|
|
567
|
+
"",
|
|
568
|
+
"第二页内容",
|
|
569
|
+
"",
|
|
570
|
+
"Third page",
|
|
571
|
+
"",
|
|
572
|
+
].join("\n"),
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("converts markdown tables into compare blocks", () => {
|
|
577
|
+
const input = [
|
|
578
|
+
"| 维度 | OpenClaw | Hermes |",
|
|
579
|
+
"| --- | --- | --- |",
|
|
580
|
+
"| 记忆 | 弱依赖 | 强依赖 |",
|
|
581
|
+
"| 可控性 | 高 | 中 |",
|
|
582
|
+
"| 成本 | 低 | 高 |",
|
|
583
|
+
].join("\n");
|
|
584
|
+
|
|
585
|
+
expect(prepareBodyForNewspic(input)).toBe(
|
|
586
|
+
[
|
|
587
|
+
"OpenClaw",
|
|
588
|
+
"- 记忆:弱依赖",
|
|
589
|
+
"- 可控性:高",
|
|
590
|
+
"- 成本:低",
|
|
591
|
+
"",
|
|
592
|
+
"Hermes",
|
|
593
|
+
"- 记忆:强依赖",
|
|
594
|
+
"- 可控性:中",
|
|
595
|
+
"- 成本:高",
|
|
596
|
+
"",
|
|
597
|
+
].join("\n"),
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("skips empty placeholder cells when converting markdown tables", () => {
|
|
602
|
+
const input = [
|
|
603
|
+
"| 项目 | 方案 A | 方案 B |",
|
|
604
|
+
"| --- | --- | --- |",
|
|
605
|
+
"| 价格 | 免费 | - |",
|
|
606
|
+
"| 部署 | 本地 | / |",
|
|
607
|
+
"| 速度 | 快 | 中 |",
|
|
608
|
+
].join("\n");
|
|
609
|
+
|
|
610
|
+
expect(prepareBodyForNewspic(input)).toBe(
|
|
611
|
+
[
|
|
612
|
+
"方案 A",
|
|
613
|
+
"- 价格:免费",
|
|
614
|
+
"- 部署:本地",
|
|
615
|
+
"- 速度:快",
|
|
616
|
+
"",
|
|
617
|
+
"方案 B",
|
|
618
|
+
"- 速度:中",
|
|
619
|
+
"",
|
|
620
|
+
].join("\n"),
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// ── buildFrontmatter ──────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
describe("buildFrontmatter", () => {
|
|
628
|
+
test("builds basic frontmatter", () => {
|
|
629
|
+
const result = buildFrontmatter({
|
|
630
|
+
title: "Hello World",
|
|
631
|
+
date: "2025-04-07",
|
|
632
|
+
platform: "wechat",
|
|
633
|
+
});
|
|
634
|
+
expect(result).toBe(
|
|
635
|
+
'---\ntitle: "Hello World"\ndate: 2025-04-07\nplatform: wechat\n---',
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("includes description when provided", () => {
|
|
640
|
+
const result = buildFrontmatter({
|
|
641
|
+
title: "Hello",
|
|
642
|
+
date: "2025-04-07",
|
|
643
|
+
platform: "blog",
|
|
644
|
+
description: "A description",
|
|
645
|
+
});
|
|
646
|
+
expect(result).toContain('description: "A description"');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("includes tags when provided", () => {
|
|
650
|
+
const result = buildFrontmatter({
|
|
651
|
+
title: "Hello",
|
|
652
|
+
date: "2025-04-07",
|
|
653
|
+
platform: "wechat",
|
|
654
|
+
tags: ["tech", "news"],
|
|
655
|
+
});
|
|
656
|
+
expect(result).toContain("tags:");
|
|
657
|
+
expect(result).toContain(" - tech");
|
|
658
|
+
expect(result).toContain(" - news");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("escapes double quotes in title", () => {
|
|
662
|
+
const result = buildFrontmatter({
|
|
663
|
+
title: 'Title with "quotes"',
|
|
664
|
+
date: "2025-04-07",
|
|
665
|
+
platform: "blog",
|
|
666
|
+
});
|
|
667
|
+
expect(result).toContain('title: "Title with \\"quotes\\""');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("omits description when null", () => {
|
|
671
|
+
const result = buildFrontmatter({
|
|
672
|
+
title: "Hello",
|
|
673
|
+
date: "2025-04-07",
|
|
674
|
+
platform: "wechat",
|
|
675
|
+
description: null,
|
|
676
|
+
});
|
|
677
|
+
expect(result).not.toContain("description:");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("omits tags when empty array", () => {
|
|
681
|
+
const result = buildFrontmatter({
|
|
682
|
+
title: "Hello",
|
|
683
|
+
date: "2025-04-07",
|
|
684
|
+
platform: "wechat",
|
|
685
|
+
tags: [],
|
|
686
|
+
});
|
|
687
|
+
expect(result).not.toContain("tags:");
|
|
688
|
+
});
|
|
689
|
+
});
|