@zzclub/pipeline 0.1.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 (98) hide show
  1. package/README.md +559 -0
  2. package/dist/assets/imgx/assets/browser/obstacle-flow.d.ts +27 -0
  3. package/dist/assets/imgx/assets/browser/obstacle-flow.js +326 -0
  4. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  5. package/dist/assets/imgx/assets/icons/clover.svg +15 -0
  6. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  7. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  8. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  9. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +22 -0
  10. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  11. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +25 -0
  12. package/dist/assets/imgx/assets/templates/ascii-portrait-3-4.html +342 -0
  13. package/dist/assets/imgx/assets/templates/ascii-portrait-tile.html +144 -0
  14. package/dist/assets/imgx/assets/templates/longform-3-4.html +179 -0
  15. package/dist/assets/imgx/assets/templates/poster-3-4.html +359 -0
  16. package/dist/assets/imgx/assets/templates/tips-3-4.html +300 -0
  17. package/dist/assets/imgx/assets/templates/wechat-cover-split.html +307 -0
  18. package/dist/assets/imgx/assets/templates/x-like-posts.html +246 -0
  19. package/dist/assets/wechat-preview/assets/browser-dist/.vite/manifest.json +314 -0
  20. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  21. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  22. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  23. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  24. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  25. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  26. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  27. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  28. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  29. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  30. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  31. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  32. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  33. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  34. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  35. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  36. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  37. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  38. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  39. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  40. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  41. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  42. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  43. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  46. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  47. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  48. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  49. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  50. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  51. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  52. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  53. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  54. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  55. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  56. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  58. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  59. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  60. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  61. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  62. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  63. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  64. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  65. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  66. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  67. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  68. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  69. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  70. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  71. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  72. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  73. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  74. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  75. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  76. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  77. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  78. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  79. package/dist/assets/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  80. package/dist/assets/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +1 -0
  81. package/dist/assets/wechat-preview/assets/browser-dist/editor-export.js +969 -0
  82. package/dist/assets/wechat-preview/assets/templates/export-shell.html +38 -0
  83. package/dist/cli.js +404 -0
  84. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +33 -0
  85. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +1063 -0
  86. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +1 -0
  87. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +175 -0
  88. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +4 -0
  89. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +979 -0
  90. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +70 -0
  91. package/dist/node_modules/@chenglou/pretext/dist/layout.js +496 -0
  92. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +36 -0
  93. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +820 -0
  94. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +28 -0
  95. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +219 -0
  96. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +51 -0
  97. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +401 -0
  98. package/package.json +42 -0
@@ -0,0 +1,307 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <!--
6
+ Template: wechat-cover-split
7
+ Ratio: 335:100 Canvas: 1340×400 Left: 940×400 Right: 400×400
8
+ 用途: 公众号文章封面长条图,左文案 + 右 icon,可裁成两张
9
+
10
+ Parameters:
11
+ {{POSTER_CONFIG_JSON}} — { blocks[]{text,highlight}, highlightWords[], highlightColor }
12
+ {{PRETEXT_MODULE_URL}} — pretext ES module 文件 URL
13
+ {{BG_COLOR}} — 背景色
14
+ {{HIGHLIGHT_COLOR}} — 高亮色(CSS .highlight 类)
15
+ {{FOOTER_TEXT}} — 底部版权/来源文字
16
+ {{ICON_PATH}} — 右侧 icon 绝对路径
17
+ {{FONT_PATH}} — 字体绝对路径
18
+ -->
19
+ <style>
20
+ @font-face {
21
+ font-family: "AlimamaShuHeiTi";
22
+ src: url("{{FONT_PATH}}") format("truetype");
23
+ }
24
+ html, body {
25
+ width: 100%;
26
+ height: 100%;
27
+ margin: 0;
28
+ overflow: hidden;
29
+ background: {{BG_COLOR}};
30
+ }
31
+ * { margin: 0; padding: 0; box-sizing: border-box; }
32
+ body {
33
+ color: #111;
34
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
35
+ opacity: 0;
36
+ transition: opacity 120ms ease;
37
+ }
38
+ body.ready { opacity: 1; }
39
+ .canvas {
40
+ position: fixed;
41
+ inset: 0;
42
+ display: flex;
43
+ background: {{BG_COLOR}};
44
+ }
45
+ .left {
46
+ width: 940px;
47
+ height: 400px;
48
+ padding: 34px 48px 28px;
49
+ display: flex;
50
+ flex-direction: column;
51
+ justify-content: center;
52
+ position: relative;
53
+ background:
54
+ radial-gradient(circle at top right, rgba(255,255,255,0.38), transparent 30%),
55
+ linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0)),
56
+ {{BG_COLOR}};
57
+ }
58
+ .eyebrow {
59
+ font-size: 18px;
60
+ letter-spacing: 0.5px;
61
+ color: rgba(0,0,0,0.38);
62
+ margin-bottom: 14px;
63
+ flex-shrink: 0;
64
+ }
65
+ .text-block {
66
+ flex-shrink: 0;
67
+ max-width: 820px;
68
+ }
69
+ .pretext-lines {
70
+ width: 100%;
71
+ }
72
+ .line {
73
+ font-size: var(--cover-font-size, 72px);
74
+ font-weight: 900;
75
+ line-height: var(--cover-line-height, 1.05);
76
+ color: #111;
77
+ letter-spacing: var(--cover-letter-spacing, -1.5px);
78
+ white-space: pre;
79
+ }
80
+ .block-text {
81
+ white-space: normal;
82
+ overflow-wrap: break-word;
83
+ }
84
+ .block + .block {
85
+ margin-top: var(--cover-block-gap, 10px);
86
+ }
87
+ .line.highlight { color: {{HIGHLIGHT_COLOR}}; }
88
+ .highlight { color: {{HIGHLIGHT_COLOR}}; }
89
+ .footer {
90
+ position: absolute;
91
+ left: 48px;
92
+ bottom: 24px;
93
+ font-size: 16px;
94
+ color: rgba(0,0,0,0.42);
95
+ letter-spacing: 1px;
96
+ }
97
+ .right {
98
+ width: 400px;
99
+ height: 400px;
100
+ border-left: 1px solid rgba(0,0,0,0.08);
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ background:
105
+ linear-gradient(180deg, rgba(255,255,255,0.34), rgba(255,255,255,0.06)),
106
+ radial-gradient(circle at top left, rgba(255,255,255,0.24), transparent 42%),
107
+ {{BG_COLOR}};
108
+ position: relative;
109
+ }
110
+ .icon-shell {
111
+ width: 232px;
112
+ height: 232px;
113
+ border-radius: 56px;
114
+ background: rgba(255,255,255,0.82);
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ box-shadow: 0 22px 50px rgba(0,0,0,0.08);
119
+ padding: 24px;
120
+ }
121
+ .icon-shell img {
122
+ width: 100%;
123
+ height: 100%;
124
+ object-fit: contain;
125
+ border-radius: 28px;
126
+ }
127
+ .grid-mark {
128
+ position: absolute;
129
+ inset: 20px;
130
+ border: 1px solid rgba(0,0,0,0.05);
131
+ border-radius: 28px;
132
+ pointer-events: none;
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div class="canvas">
138
+ <section class="left">
139
+ <div class="eyebrow">公众号·早早集市</div>
140
+ <div class="text-block">
141
+ <div class="pretext-lines" id="cover-lines" aria-label="cover text"></div>
142
+ </div>
143
+ <div class="footer">{{FOOTER_TEXT}}</div>
144
+ </section>
145
+
146
+ <aside class="right">
147
+ <div class="grid-mark"></div>
148
+ <div class="icon-shell">
149
+ <img src="{{ICON_PATH}}" alt="icon">
150
+ </div>
151
+ </aside>
152
+ </div>
153
+
154
+ <script id="poster-config" type="application/json">{{POSTER_CONFIG_JSON}}</script>
155
+ <script type="module">
156
+ import { prepareWithSegments, layoutWithLines } from "{{PRETEXT_MODULE_URL}}";
157
+
158
+ const configNode = document.getElementById("poster-config");
159
+ const linesRoot = document.getElementById("cover-lines");
160
+ const textBlock = document.querySelector(".text-block");
161
+ if (
162
+ !(configNode instanceof HTMLScriptElement) ||
163
+ !(linesRoot instanceof HTMLDivElement) ||
164
+ !(textBlock instanceof HTMLDivElement)
165
+ ) {
166
+ throw new Error("cover nodes not found");
167
+ }
168
+
169
+ const config = JSON.parse(configNode.textContent || "{}");
170
+ const blocks = Array.isArray(config.blocks) ? config.blocks : [];
171
+ const highlightWords = Array.isArray(config.highlightWords) ? config.highlightWords.filter(Boolean) : [];
172
+
173
+ // ── Preset: wechat-cover-split (3.35:1 横幅) ────────────────────────────
174
+ // 内容(文字/高亮)来自 POSTER_CONFIG_JSON;
175
+ // 以下常量是本比例的 layout preset,决定该模板的字号范围与约束。
176
+ const FONT_FAMILY = '"AlimamaShuHeiTi", "PingFang SC", sans-serif';
177
+ const FONT_WEIGHT = 900;
178
+ const MIN_FONT_SIZE = 36; // 超长标题兜底下限
179
+ const MAX_FONT_SIZE = 88; // 横幅高度决定的上限
180
+ const MAX_LINES_PER_BLOCK = 2; // 每块最多 2 行(render-card 拆分也保证此限制)
181
+ const MAX_WIDTH = 820; // 对应 CSS .text-block max-width
182
+ const FALLBACK_AVAILABLE_HEIGHT = 248;
183
+ // ────────────────────────────────────────────────────────────────────────
184
+
185
+ function escapeHtml(value) {
186
+ return value
187
+ .replaceAll("&", "&amp;")
188
+ .replaceAll("<", "&lt;")
189
+ .replaceAll(">", "&gt;")
190
+ .replaceAll('"', "&quot;")
191
+ .replaceAll("'", "&#39;");
192
+ }
193
+
194
+ function escapeRegExp(value) {
195
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
196
+ }
197
+
198
+ function getLineHeight(fontSize) {
199
+ if (fontSize >= 80) return Math.round(fontSize * 1.06);
200
+ if (fontSize >= 60) return Math.round(fontSize * 1.05);
201
+ return Math.round(fontSize * 1.04);
202
+ }
203
+
204
+ function getBlockGap(fontSize) {
205
+ return fontSize >= 64 ? 12 : fontSize >= 52 ? 10 : 8;
206
+ }
207
+
208
+ function getLetterSpacing(fontSize) {
209
+ if (fontSize >= 80) return "-2px";
210
+ if (fontSize >= 64) return "-1.5px";
211
+ if (fontSize >= 48) return "-1px";
212
+ return "-0.5px";
213
+ }
214
+
215
+ function measureLayout(fontSize, maxWidth) {
216
+ const lineHeight = getLineHeight(fontSize);
217
+ const blockGap = getBlockGap(fontSize);
218
+ const font = `${FONT_WEIGHT} ${fontSize}px ${FONT_FAMILY}`;
219
+ let totalHeight = 0;
220
+ let valid = true;
221
+ const renderedBlocks = [];
222
+
223
+ for (let i = 0; i < blocks.length; i++) {
224
+ const block = blocks[i];
225
+ const prepared = prepareWithSegments(block.text, font, { whiteSpace: "pre-wrap" });
226
+ const result = layoutWithLines(prepared, maxWidth, lineHeight);
227
+ if (result.lines.length > MAX_LINES_PER_BLOCK) valid = false;
228
+ renderedBlocks.push({
229
+ highlight : !!block.highlight,
230
+ text : block.text,
231
+ lineCount : result.lines.length,
232
+ widths : result.lines.map(l => l.width),
233
+ });
234
+ totalHeight += result.height;
235
+ if (i > 0) totalHeight += blockGap;
236
+ }
237
+
238
+ return { font, fontSize, lineHeight, blockGap, maxWidth, totalHeight, blocks: renderedBlocks, valid };
239
+ }
240
+
241
+ function highlightHtml(text) {
242
+ let html = escapeHtml(text);
243
+ for (const word of highlightWords) {
244
+ const safe = String(word || "").trim();
245
+ if (!safe) continue;
246
+ html = html.replace(
247
+ new RegExp(escapeRegExp(escapeHtml(safe)), "g"),
248
+ `<span class="highlight">${escapeHtml(safe)}</span>`,
249
+ );
250
+ }
251
+ return html;
252
+ }
253
+
254
+ function renderLayout(layout) {
255
+ document.body.style.setProperty("--cover-font-size", `${layout.fontSize}px`);
256
+ document.body.style.setProperty("--cover-line-height", String(layout.lineHeight / layout.fontSize));
257
+ document.body.style.setProperty("--cover-letter-spacing", getLetterSpacing(layout.fontSize));
258
+ document.body.style.setProperty("--cover-block-gap", `${layout.blockGap}px`);
259
+
260
+ linesRoot.innerHTML = layout.blocks
261
+ .map(block => {
262
+ const cls = block.highlight ? "line block-text highlight" : "line block-text";
263
+ return `<div class="block"><div class="${cls}">${highlightHtml(block.text)}</div></div>`;
264
+ })
265
+ .join("");
266
+ }
267
+
268
+ async function boot() {
269
+ await document.fonts.ready;
270
+
271
+ const maxWidth = Math.min(MAX_WIDTH, Math.floor(textBlock.clientWidth));
272
+ const leftEl = document.querySelector(".left");
273
+ const eyebrowEl = document.querySelector(".eyebrow");
274
+ const leftH = leftEl ? leftEl.clientHeight : 400;
275
+ const eyebrowH = eyebrowEl ? Math.ceil(eyebrowEl.getBoundingClientRect().height) + 14 : 32;
276
+ const padV = 34 + 28; // .left padding-top + padding-bottom
277
+ const footerRes = 52; // footer 16px 字 + bottom 24px + buffer
278
+ const availableHeight = Math.max(
279
+ FALLBACK_AVAILABLE_HEIGHT,
280
+ leftH - padV - eyebrowH - footerRes,
281
+ );
282
+
283
+ let chosen = null;
284
+
285
+ // Pass 1:最大字号,同时满足行数约束 + 高度约束
286
+ for (let fs = MAX_FONT_SIZE; fs >= MIN_FONT_SIZE; fs -= 2) {
287
+ const layout = measureLayout(fs, maxWidth);
288
+ if (layout.valid && layout.totalHeight <= availableHeight) { chosen = layout; break; }
289
+ }
290
+ // Pass 2:放宽高度约束,仍满足行数约束(超长标题兜底)
291
+ if (!chosen) {
292
+ for (let fs = MAX_FONT_SIZE; fs >= MIN_FONT_SIZE; fs -= 2) {
293
+ const layout = measureLayout(fs, maxWidth);
294
+ if (layout.valid) { chosen = layout; break; }
295
+ }
296
+ }
297
+ // Pass 3:无条件兜底(文字极长时)
298
+ if (!chosen) chosen = measureLayout(MIN_FONT_SIZE, maxWidth);
299
+
300
+ renderLayout(chosen);
301
+ document.body.classList.add("ready");
302
+ }
303
+
304
+ boot();
305
+ </script>
306
+ </body>
307
+ </html>
@@ -0,0 +1,246 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <!--
6
+ Template: x-like-posts
7
+ Width: 900px
8
+ Height: dynamic
9
+ 用途: 多条帖子转成长图,模仿 X / Twitter 极简分享排版
10
+ Parameters:
11
+ {{BG_COLOR}} — 页面背景色
12
+ {{CARD_BG}} — 卡片背景色
13
+ {{TEXT_COLOR}} — 正文颜色
14
+ {{MUTED_COLOR}} — 次级文字颜色
15
+ {{BORDER_COLOR}} — 卡片分隔线颜色
16
+ {{ACCENT_COLOR}} — 品牌强调色
17
+ {{AUTHOR_NAME}} — 作者名
18
+ {{AUTHOR_HANDLE}} — 作者 handle
19
+ {{AUTHOR_AVATAR}} — 头像绝对路径
20
+ {{HEADER_LABEL}} — 顶部说明
21
+ {{TIME_RANGE_LABEL}} — 顶部日期
22
+ {{FOOTER_TEXT}} — 底部来源
23
+ {{FONT_PATH}} — 标题字体
24
+ {{TWEET_ITEMS}} — 帖子列表 HTML
25
+ -->
26
+ <style>
27
+ @font-face {
28
+ font-family: "AlimamaShuHeiTi";
29
+ src: url("{{FONT_PATH}}") format("truetype");
30
+ }
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body {
33
+ width: 900px;
34
+ background: {{BG_COLOR}};
35
+ color: {{TEXT_COLOR}};
36
+ padding: 48px;
37
+ font-family: "TwitterChirp", "SF Pro Display", "Helvetica Neue", "PingFang SC", sans-serif;
38
+ }
39
+ .sheet {
40
+ width: 100%;
41
+ background: {{CARD_BG}};
42
+ border: 1px solid {{BORDER_COLOR}};
43
+ border-radius: 36px;
44
+ overflow: hidden;
45
+ box-shadow: 0 28px 60px rgba(15, 20, 25, 0.08);
46
+ }
47
+ .hero {
48
+ padding: 34px 36px 24px;
49
+ border-bottom: 1px solid {{BORDER_COLOR}};
50
+ background:
51
+ radial-gradient(circle at top right, rgba(29, 155, 240, 0.09), transparent 34%),
52
+ linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,1));
53
+ }
54
+ .hero-label {
55
+ display: inline-flex;
56
+ align-items: center;
57
+ gap: 8px;
58
+ font-size: 18px;
59
+ color: {{MUTED_COLOR}};
60
+ margin-bottom: 18px;
61
+ }
62
+ .hero-label .dot {
63
+ width: 8px;
64
+ height: 8px;
65
+ border-radius: 999px;
66
+ background: {{ACCENT_COLOR}};
67
+ }
68
+ .hero-title {
69
+ font-family: "AlimamaShuHeiTi", "SF Pro Display", sans-serif;
70
+ font-size: 44px;
71
+ line-height: 1.12;
72
+ letter-spacing: -1px;
73
+ margin-bottom: 18px;
74
+ }
75
+ .hero-meta {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 16px;
79
+ }
80
+ .hero-meta img {
81
+ width: 56px;
82
+ height: 56px;
83
+ border-radius: 50%;
84
+ object-fit: cover;
85
+ background: #eef3f4;
86
+ }
87
+ .hero-meta-text {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 3px;
91
+ }
92
+ .hero-meta-text .name {
93
+ font-size: 24px;
94
+ font-weight: 700;
95
+ color: {{TEXT_COLOR}};
96
+ }
97
+ .hero-meta-text .handle {
98
+ font-size: 20px;
99
+ color: {{MUTED_COLOR}};
100
+ }
101
+ .hero-time {
102
+ margin-top: 14px;
103
+ font-size: 18px;
104
+ color: {{MUTED_COLOR}};
105
+ letter-spacing: 0.2px;
106
+ }
107
+ .thread {
108
+ padding: 0 36px;
109
+ }
110
+ .tweet {
111
+ display: flex;
112
+ gap: 18px;
113
+ padding: 26px 0;
114
+ border-bottom: 1px solid {{BORDER_COLOR}};
115
+ }
116
+ .tweet:last-child {
117
+ border-bottom: none;
118
+ }
119
+ .avatar-wrap {
120
+ width: 58px;
121
+ flex-shrink: 0;
122
+ }
123
+ .avatar-wrap img {
124
+ width: 58px;
125
+ height: 58px;
126
+ border-radius: 50%;
127
+ object-fit: cover;
128
+ background: #eef3f4;
129
+ }
130
+ .tweet-main {
131
+ flex: 1;
132
+ min-width: 0;
133
+ }
134
+ .tweet-top {
135
+ display: flex;
136
+ align-items: baseline;
137
+ flex-wrap: wrap;
138
+ gap: 8px;
139
+ margin-bottom: 10px;
140
+ line-height: 1.25;
141
+ }
142
+ .tweet-top .name {
143
+ font-size: 23px;
144
+ font-weight: 700;
145
+ }
146
+ .tweet-top .handle,
147
+ .tweet-top .meta {
148
+ font-size: 20px;
149
+ color: {{MUTED_COLOR}};
150
+ }
151
+ .tweet-text {
152
+ font-size: 30px;
153
+ line-height: 1.55;
154
+ color: {{TEXT_COLOR}};
155
+ word-break: break-word;
156
+ white-space: normal;
157
+ }
158
+ .tweet-text p + p {
159
+ margin-top: 16px;
160
+ }
161
+ .tweet-text a {
162
+ color: {{ACCENT_COLOR}};
163
+ text-decoration: none;
164
+ }
165
+ .tweet-footer {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: space-between;
169
+ gap: 12px;
170
+ margin-top: 18px;
171
+ font-size: 18px;
172
+ color: {{MUTED_COLOR}};
173
+ }
174
+ .tweet-index {
175
+ color: {{ACCENT_COLOR}};
176
+ font-weight: 700;
177
+ }
178
+ .tweet-stats {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ flex-wrap: wrap;
182
+ justify-content: flex-end;
183
+ gap: 12px;
184
+ text-align: right;
185
+ }
186
+ .tweet-stats .stat strong {
187
+ font-weight: 700;
188
+ color: {{TEXT_COLOR}};
189
+ margin-right: 4px;
190
+ }
191
+ .sheet-footer {
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: center;
195
+ gap: 20px;
196
+ padding: 22px 36px 28px;
197
+ border-top: 1px solid {{BORDER_COLOR}};
198
+ color: {{MUTED_COLOR}};
199
+ font-size: 18px;
200
+ background: linear-gradient(180deg, rgba(248,250,251,0.86), rgba(255,255,255,1));
201
+ }
202
+ .brand {
203
+ display: inline-flex;
204
+ align-items: center;
205
+ gap: 8px;
206
+ }
207
+ .brand-mark {
208
+ width: 10px;
209
+ height: 10px;
210
+ border-radius: 999px;
211
+ background: {{ACCENT_COLOR}};
212
+ }
213
+ </style>
214
+ </head>
215
+ <body>
216
+ <section class="sheet">
217
+ <header class="hero">
218
+ <div class="hero-label">
219
+ <span class="dot"></span>
220
+ <span>{{HEADER_LABEL}}</span>
221
+ </div>
222
+ <h1 class="hero-title">X-like Posts</h1>
223
+ <div class="hero-meta">
224
+ <img src="{{AUTHOR_AVATAR}}" alt="avatar">
225
+ <div class="hero-meta-text">
226
+ <span class="name">{{AUTHOR_NAME}}</span>
227
+ <span class="handle">{{AUTHOR_HANDLE}}</span>
228
+ </div>
229
+ </div>
230
+ <div class="hero-time">{{TIME_RANGE_LABEL}}</div>
231
+ </header>
232
+
233
+ <main class="thread">
234
+ {{TWEET_ITEMS}}
235
+ </main>
236
+
237
+ <footer class="sheet-footer">
238
+ <div class="brand">
239
+ <span class="brand-mark"></span>
240
+ <span>{{FOOTER_TEXT}}</span>
241
+ </div>
242
+ <span>X-like share export</span>
243
+ </footer>
244
+ </section>
245
+ </body>
246
+ </html>