@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
|
@@ -164,10 +164,10 @@
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
function getLetterSpacing(fontSize) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
167
|
+
const t = (fontSize - 34) / (132 - 34);
|
|
168
|
+
const clamped = Math.min(1, Math.max(0, t));
|
|
169
|
+
const spacing = -2.8 + clamped * 2.6;
|
|
170
|
+
return `${spacing.toFixed(1)}px`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function measureLayout(fontSize, maxWidth) {
|
|
@@ -202,26 +202,42 @@
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
// Character width coefficients for visual estimation.
|
|
206
|
+
// CJK characters are roughly 2x the width of Latin characters in CJK fonts.
|
|
207
|
+
const CJK_RE = /[一-鿿㐀-䶿豈- -〿-]/;
|
|
208
|
+
const LATIN_WIDE_RE = /[A-Z0-9@#%&]/;
|
|
209
|
+
const LATIN_NARROW_RE = /[a-z \t\n\r.,;:!?()[\]{}\-+=*/\\|]/;
|
|
210
|
+
|
|
211
|
+
function estimateVisualWidth(text) {
|
|
212
|
+
let width = 0;
|
|
213
|
+
for (const ch of Array.from(text)) {
|
|
214
|
+
if (CJK_RE.test(ch)) {
|
|
215
|
+
width += 2.0;
|
|
216
|
+
} else if (LATIN_WIDE_RE.test(ch)) {
|
|
217
|
+
width += 1.0;
|
|
218
|
+
} else if (LATIN_NARROW_RE.test(ch)) {
|
|
219
|
+
width += 0.55;
|
|
220
|
+
} else {
|
|
221
|
+
width += 1.0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return width;
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
function getDesiredLineRange(
|
|
212
|
-
if (
|
|
213
|
-
if (
|
|
214
|
-
if (
|
|
215
|
-
if (
|
|
216
|
-
if (
|
|
227
|
+
function getDesiredLineRange(visualWidth) {
|
|
228
|
+
if (visualWidth <= 16) return [1, 2];
|
|
229
|
+
if (visualWidth <= 32) return [2, 3];
|
|
230
|
+
if (visualWidth <= 50) return [3, 4];
|
|
231
|
+
if (visualWidth <= 72) return [4, 6];
|
|
232
|
+
if (visualWidth <= 100) return [5, 7];
|
|
217
233
|
return [6, 8];
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
function getBlockMaxWidth(
|
|
221
|
-
if (
|
|
222
|
-
if (
|
|
223
|
-
if (
|
|
224
|
-
if (
|
|
236
|
+
function getBlockMaxWidth(visualWidth) {
|
|
237
|
+
if (visualWidth <= 18) return Math.floor(textBlock.clientWidth);
|
|
238
|
+
if (visualWidth <= 38) return Math.floor(textBlock.clientWidth * 0.92);
|
|
239
|
+
if (visualWidth <= 60) return Math.floor(textBlock.clientWidth * 0.86);
|
|
240
|
+
if (visualWidth <= 90) return Math.floor(textBlock.clientWidth * 0.80);
|
|
225
241
|
return Math.floor(textBlock.clientWidth * 0.74);
|
|
226
242
|
}
|
|
227
243
|
|
|
@@ -229,28 +245,51 @@
|
|
|
229
245
|
if (layout.totalHeight > availableHeight) return Number.POSITIVE_INFINITY;
|
|
230
246
|
|
|
231
247
|
let score = 0;
|
|
248
|
+
|
|
249
|
+
// 1. Optical centering: text block should sit slightly above mathematical center
|
|
250
|
+
// (~46% from top, not 50%) — this is a well-established magazine design rule.
|
|
251
|
+
const opticalCenter = availableHeight * 0.46;
|
|
252
|
+
const topPadding = (availableHeight - layout.totalHeight) / 2;
|
|
253
|
+
const centerError = Math.abs(topPadding - opticalCenter) / availableHeight;
|
|
254
|
+
score += centerError * 400;
|
|
255
|
+
|
|
256
|
+
// 2. Per-block line quality
|
|
232
257
|
for (const block of layout.blocks) {
|
|
233
|
-
const
|
|
234
|
-
const [minLines, maxLines] = getDesiredLineRange(
|
|
258
|
+
const visualWidth = estimateVisualWidth(block.text);
|
|
259
|
+
const [minLines, maxLines] = getDesiredLineRange(visualWidth);
|
|
235
260
|
const lineCount = block.lineCount;
|
|
236
|
-
|
|
237
|
-
if (lineCount
|
|
261
|
+
|
|
262
|
+
if (lineCount < minLines) score += (minLines - lineCount) * 80;
|
|
263
|
+
if (lineCount > maxLines) score += (lineCount - maxLines) * 120;
|
|
238
264
|
|
|
239
265
|
if (block.widths.length > 1) {
|
|
240
266
|
const widest = Math.max(...block.widths);
|
|
241
267
|
const narrowest = Math.min(...block.widths);
|
|
242
268
|
const fillRatio = widest / Math.max(1, maxWidth);
|
|
243
269
|
const raggedRatio = narrowest / Math.max(1, widest);
|
|
244
|
-
|
|
245
|
-
|
|
270
|
+
|
|
271
|
+
// Last line sweet spot: 65-85% of widest line
|
|
272
|
+
const lastLine = block.widths[block.widths.length - 1];
|
|
273
|
+
const lastRatio = lastLine / Math.max(1, widest);
|
|
274
|
+
score += Math.abs(lastRatio - 0.75) * 150;
|
|
275
|
+
|
|
276
|
+
score += Math.max(0, 0.78 - fillRatio) * 180;
|
|
277
|
+
score += Math.max(0, 0.55 - raggedRatio) * 170;
|
|
246
278
|
} else if (block.widths.length === 1) {
|
|
247
279
|
const fillRatio = block.widths[0] / Math.max(1, maxWidth);
|
|
248
|
-
if (
|
|
280
|
+
if (visualWidth > 12) {
|
|
281
|
+
score += Math.abs(fillRatio - 0.84) * 200;
|
|
282
|
+
}
|
|
249
283
|
}
|
|
250
284
|
}
|
|
251
285
|
|
|
252
|
-
|
|
253
|
-
|
|
286
|
+
// 3. Height utilization sweet spot: 80-92%
|
|
287
|
+
const heightRatio = layout.totalHeight / Math.max(1, availableHeight);
|
|
288
|
+
score += Math.abs(heightRatio - 0.86) * 80;
|
|
289
|
+
|
|
290
|
+
// 4. Slight bias toward larger fonts
|
|
291
|
+
score -= (layout.fontSize / 132) * 5;
|
|
292
|
+
|
|
254
293
|
return score;
|
|
255
294
|
}
|
|
256
295
|
|
|
@@ -328,8 +367,8 @@
|
|
|
328
367
|
|
|
329
368
|
async function boot() {
|
|
330
369
|
await document.fonts.ready;
|
|
331
|
-
const
|
|
332
|
-
const maxWidth = getBlockMaxWidth(
|
|
370
|
+
const sourceVisualWidth = blocks.reduce((sum, block) => sum + estimateVisualWidth(block.text), 0);
|
|
371
|
+
const maxWidth = getBlockMaxWidth(sourceVisualWidth);
|
|
333
372
|
const iconHeight = Math.ceil(iconNode.getBoundingClientRect().height);
|
|
334
373
|
const watermarkHeight = Math.ceil(watermarkNode.getBoundingClientRect().height);
|
|
335
374
|
const availableHeight = Math.max(
|
|
@@ -196,20 +196,23 @@
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
function getLineHeight(fontSize) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
// Smooth interpolation: 1.04 at 36px → 1.08 at 88px
|
|
200
|
+
const t = (fontSize - 36) / (88 - 36);
|
|
201
|
+
const ratio = 1.04 + Math.min(1, Math.max(0, t)) * 0.04;
|
|
202
|
+
return Math.round(fontSize * ratio);
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
function getBlockGap(fontSize) {
|
|
205
|
-
|
|
206
|
+
// Smooth: 8px at 36px → 14px at 88px
|
|
207
|
+
const t = (fontSize - 36) / (88 - 36);
|
|
208
|
+
return Math.round(8 + Math.min(1, Math.max(0, t)) * 6);
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
function getLetterSpacing(fontSize) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return
|
|
212
|
+
// Smooth: -0.5px at 36px → -2.5px at 88px
|
|
213
|
+
const t = (fontSize - 36) / (88 - 36);
|
|
214
|
+
const val = -0.5 + Math.min(1, Math.max(0, t)) * (-2.0);
|
|
215
|
+
return `${val.toFixed(1)}px`;
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
function measureLayout(fontSize, maxWidth) {
|
|
@@ -265,39 +268,67 @@
|
|
|
265
268
|
.join("");
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
function scoreCoverLayout(layout, availableHeight, maxWidth) {
|
|
272
|
+
let score = 0;
|
|
273
|
+
|
|
274
|
+
// 1. Height within sweet zone (75-90%)
|
|
275
|
+
const heightRatio = layout.totalHeight / Math.max(1, availableHeight);
|
|
276
|
+
score += Math.abs(heightRatio - 0.82) * 200;
|
|
277
|
+
|
|
278
|
+
// 2. Per-block line quality
|
|
279
|
+
for (const block of layout.blocks) {
|
|
280
|
+
if (block.widths.length === 1) {
|
|
281
|
+
const fill = block.widths[0] / maxWidth;
|
|
282
|
+
score += Math.abs(fill - 0.85) * 150;
|
|
283
|
+
} else if (block.widths.length === 2) {
|
|
284
|
+
const line1Fill = block.widths[0] / maxWidth;
|
|
285
|
+
const line2Fill = block.widths[1] / maxWidth;
|
|
286
|
+
// Prefer pyramid: line1 wider than line2
|
|
287
|
+
if (line2Fill > line1Fill) score += 80;
|
|
288
|
+
// Line2 should be 60-85% of line1
|
|
289
|
+
const ratio = block.widths[1] / Math.max(1, block.widths[0]);
|
|
290
|
+
score += Math.abs(ratio - 0.72) * 180;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 3. Slight bias toward larger fonts
|
|
295
|
+
score -= (layout.fontSize / 88) * 10;
|
|
296
|
+
|
|
297
|
+
return score;
|
|
298
|
+
}
|
|
299
|
+
|
|
268
300
|
async function boot() {
|
|
269
301
|
await document.fonts.ready;
|
|
270
302
|
|
|
271
|
-
const maxWidth
|
|
272
|
-
const leftEl
|
|
303
|
+
const maxWidth = Math.min(MAX_WIDTH, Math.floor(textBlock.clientWidth));
|
|
304
|
+
const leftEl = document.querySelector(".left");
|
|
273
305
|
const eyebrowEl = document.querySelector(".eyebrow");
|
|
274
|
-
const leftH
|
|
275
|
-
const eyebrowH
|
|
276
|
-
const padV
|
|
277
|
-
const footerRes = 52;
|
|
306
|
+
const leftH = leftEl ? leftEl.clientHeight : 400;
|
|
307
|
+
const eyebrowH = eyebrowEl ? Math.ceil(eyebrowEl.getBoundingClientRect().height) + 14 : 32;
|
|
308
|
+
const padV = 34 + 28;
|
|
309
|
+
const footerRes = 52;
|
|
278
310
|
const availableHeight = Math.max(
|
|
279
311
|
FALLBACK_AVAILABLE_HEIGHT,
|
|
280
312
|
leftH - padV - eyebrowH - footerRes,
|
|
281
313
|
);
|
|
282
314
|
|
|
283
|
-
let
|
|
315
|
+
let best = null;
|
|
316
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
284
317
|
|
|
285
|
-
|
|
286
|
-
for (let fs = MAX_FONT_SIZE; fs >= MIN_FONT_SIZE; fs -= 2) {
|
|
318
|
+
for (let fs = MAX_FONT_SIZE; fs >= MIN_FONT_SIZE; fs -= 1) {
|
|
287
319
|
const layout = measureLayout(fs, maxWidth);
|
|
288
|
-
if (layout.valid
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (layout.valid) { chosen = layout; break; }
|
|
320
|
+
if (!layout.valid) continue;
|
|
321
|
+
|
|
322
|
+
const score = scoreCoverLayout(layout, availableHeight, maxWidth);
|
|
323
|
+
if (score < bestScore) {
|
|
324
|
+
bestScore = score;
|
|
325
|
+
best = layout;
|
|
295
326
|
}
|
|
296
327
|
}
|
|
297
|
-
// Pass 3:无条件兜底(文字极长时)
|
|
298
|
-
if (!chosen) chosen = measureLayout(MIN_FONT_SIZE, maxWidth);
|
|
299
328
|
|
|
300
|
-
|
|
329
|
+
if (!best) best = measureLayout(MIN_FONT_SIZE, maxWidth);
|
|
330
|
+
|
|
331
|
+
renderLayout(best);
|
|
301
332
|
document.body.classList.add("ready");
|
|
302
333
|
}
|
|
303
334
|
|
package/src/imgx/cli.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ParsedArgs = {
|
|
2
|
+
flags: Map<string, string[]>;
|
|
3
|
+
booleans: Set<string>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
7
|
+
const flags = new Map<string, string[]>();
|
|
8
|
+
const booleans = new Set<string>();
|
|
9
|
+
|
|
10
|
+
for (let index = 0; index < argv.length; index++) {
|
|
11
|
+
const token = argv[index];
|
|
12
|
+
if (!token?.startsWith("--")) continue;
|
|
13
|
+
|
|
14
|
+
const name = token.slice(2);
|
|
15
|
+
const next = argv[index + 1];
|
|
16
|
+
if (next === undefined || next.startsWith("--")) {
|
|
17
|
+
booleans.add(name);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const current = flags.get(name) ?? [];
|
|
22
|
+
current.push(next);
|
|
23
|
+
flags.set(name, current);
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { flags, booleans };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getArg(parsed: ParsedArgs, name: string, fallback = ""): string {
|
|
31
|
+
const values = parsed.flags.get(name);
|
|
32
|
+
if (values === undefined || values.length === 0) return fallback;
|
|
33
|
+
return values[values.length - 1] ?? fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getArgs(parsed: ParsedArgs, name: string): string[] {
|
|
37
|
+
return parsed.flags.get(name) ?? [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasFlag(parsed: ParsedArgs, name: string): boolean {
|
|
41
|
+
return parsed.booleans.has(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function requireArg(parsed: ParsedArgs, name: string): string {
|
|
45
|
+
const value = getArg(parsed, name);
|
|
46
|
+
if (value.length === 0) {
|
|
47
|
+
throw new Error(`Missing required argument --${name}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getIntArg(parsed: ParsedArgs, name: string, fallback: number): number {
|
|
53
|
+
const raw = getArg(parsed, name);
|
|
54
|
+
if (raw.length === 0) return fallback;
|
|
55
|
+
const value = Number.parseInt(raw, 10);
|
|
56
|
+
return Number.isFinite(value) ? value : fallback;
|
|
57
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LongformGeometry, LongformTheme } from "./longform-theme";
|
|
2
|
+
|
|
3
|
+
export type LongformGeometryOverrides = Partial<{
|
|
4
|
+
pageWidth: number;
|
|
5
|
+
pageHeight: number;
|
|
6
|
+
bodyPaddingX: number;
|
|
7
|
+
bodyPaddingY: number;
|
|
8
|
+
logoSize: number;
|
|
9
|
+
logoGap: number;
|
|
10
|
+
footerMarginTop: number;
|
|
11
|
+
footerHeight: number;
|
|
12
|
+
contentBottomGap: number;
|
|
13
|
+
contentWidth: number;
|
|
14
|
+
contentHeight: number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
function sanitizePositiveInt(value: number | undefined | null): number | null {
|
|
18
|
+
if (!Number.isFinite(value)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const rounded = Math.round(value!);
|
|
22
|
+
return rounded > 0 ? rounded : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function applyGeometryOverrides(theme: LongformTheme, overrides: LongformGeometryOverrides): LongformTheme {
|
|
26
|
+
return {
|
|
27
|
+
...theme,
|
|
28
|
+
pageWidth: sanitizePositiveInt(overrides.pageWidth) ?? theme.pageWidth,
|
|
29
|
+
pageHeight: sanitizePositiveInt(overrides.pageHeight) ?? theme.pageHeight,
|
|
30
|
+
bodyPaddingX: sanitizePositiveInt(overrides.bodyPaddingX) ?? theme.bodyPaddingX,
|
|
31
|
+
bodyPaddingY: sanitizePositiveInt(overrides.bodyPaddingY) ?? theme.bodyPaddingY,
|
|
32
|
+
logoSize: sanitizePositiveInt(overrides.logoSize) ?? theme.logoSize,
|
|
33
|
+
logoGap: sanitizePositiveInt(overrides.logoGap) ?? theme.logoGap,
|
|
34
|
+
footerMarginTop: sanitizePositiveInt(overrides.footerMarginTop) ?? theme.footerMarginTop,
|
|
35
|
+
footerHeight: sanitizePositiveInt(overrides.footerHeight) ?? theme.footerHeight,
|
|
36
|
+
contentBottomGap: sanitizePositiveInt(overrides.contentBottomGap) ?? theme.contentBottomGap,
|
|
37
|
+
contentWidth: sanitizePositiveInt(overrides.contentWidth) ?? theme.contentWidth,
|
|
38
|
+
contentHeight: sanitizePositiveInt(overrides.contentHeight) ?? theme.contentHeight,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getDerivedGeometry(theme: LongformTheme): LongformGeometry {
|
|
43
|
+
const pageWidth = theme.pageWidth;
|
|
44
|
+
const pageHeight = theme.pageHeight;
|
|
45
|
+
const bodyWidth = Math.max(120, pageWidth - theme.bodyPaddingX * 2);
|
|
46
|
+
const headerHeight = theme.logoSize + theme.logoGap;
|
|
47
|
+
const footerReservedHeight = theme.footerHeight + theme.footerMarginTop;
|
|
48
|
+
const derivedContentHeight = Math.max(
|
|
49
|
+
120,
|
|
50
|
+
pageHeight - theme.bodyPaddingY * 2 - headerHeight - footerReservedHeight,
|
|
51
|
+
);
|
|
52
|
+
const contentWidth = Math.min(theme.contentWidth ?? bodyWidth, bodyWidth);
|
|
53
|
+
const contentHeight = Math.min(theme.contentHeight ?? derivedContentHeight, derivedContentHeight);
|
|
54
|
+
return {
|
|
55
|
+
pageWidth,
|
|
56
|
+
pageHeight,
|
|
57
|
+
bodyPaddingX: theme.bodyPaddingX,
|
|
58
|
+
bodyPaddingY: theme.bodyPaddingY,
|
|
59
|
+
logoSize: theme.logoSize,
|
|
60
|
+
logoGap: theme.logoGap,
|
|
61
|
+
footerMarginTop: theme.footerMarginTop,
|
|
62
|
+
footerHeight: theme.footerHeight,
|
|
63
|
+
contentBottomGap: theme.contentBottomGap,
|
|
64
|
+
contentWidth,
|
|
65
|
+
contentHeight,
|
|
66
|
+
contentStageHeight: Math.max(80, contentHeight - theme.contentBottomGap),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { runRenderArticleCli } from "./render-article";
|
|
2
|
+
export type { RenderArticleResult } from "./render-article";
|
|
3
|
+
export { runRenderAsciiPortraitCli } from "./render-ascii-portrait";
|
|
4
|
+
export { runRenderCardCli } from "./render-card";
|
|
5
|
+
export { runRenderXLikePostsCli } from "./render-x-like-posts";
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { computeSpacing, proportionalLineHeight } from "./typographic-scale";
|
|
2
|
+
|
|
3
|
+
export type ContentBlockKind = "paragraph" | "heading" | "quote" | "list-item";
|
|
4
|
+
|
|
5
|
+
export const LONGFORM_PAGE_WIDTH = 900;
|
|
6
|
+
export const LONGFORM_PAGE_HEIGHT = 1200;
|
|
7
|
+
|
|
8
|
+
export type LongformLineStyle = {
|
|
9
|
+
font: string;
|
|
10
|
+
lineHeight: number;
|
|
11
|
+
className: string;
|
|
12
|
+
gapBefore?: number;
|
|
13
|
+
gapAfter?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type LongformTheme = {
|
|
17
|
+
name: string;
|
|
18
|
+
bgColor: string;
|
|
19
|
+
bodyColor: string;
|
|
20
|
+
accentColor: string;
|
|
21
|
+
quoteColor: string;
|
|
22
|
+
captionColor: string;
|
|
23
|
+
watermarkColor: string;
|
|
24
|
+
watermarkOpacity: number;
|
|
25
|
+
pageWidth: number;
|
|
26
|
+
pageHeight: number;
|
|
27
|
+
imageRadius: number;
|
|
28
|
+
bodyPaddingX: number;
|
|
29
|
+
bodyPaddingY: number;
|
|
30
|
+
logoSize: number;
|
|
31
|
+
logoGap: number;
|
|
32
|
+
footerMarginTop: number;
|
|
33
|
+
footerHeight: number;
|
|
34
|
+
contentBottomGap: number;
|
|
35
|
+
contentWidth: number | null;
|
|
36
|
+
contentHeight: number | null;
|
|
37
|
+
bodyStyles: Record<ContentBlockKind, LongformLineStyle>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type LongformGeometry = {
|
|
41
|
+
pageWidth: number;
|
|
42
|
+
pageHeight: number;
|
|
43
|
+
bodyPaddingX: number;
|
|
44
|
+
bodyPaddingY: number;
|
|
45
|
+
logoSize: number;
|
|
46
|
+
logoGap: number;
|
|
47
|
+
footerMarginTop: number;
|
|
48
|
+
footerHeight: number;
|
|
49
|
+
contentBottomGap: number;
|
|
50
|
+
contentWidth: number;
|
|
51
|
+
contentHeight: number;
|
|
52
|
+
contentStageHeight: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return a new theme with contentHeight overridden.
|
|
57
|
+
* Used when min_pages > 1 to shrink the per-page content area so the
|
|
58
|
+
* pagination engine naturally splits content across more pages.
|
|
59
|
+
*/
|
|
60
|
+
export function applyContentHeightOverride(theme: LongformTheme, contentHeight: number): LongformTheme {
|
|
61
|
+
return { ...theme, contentHeight: Math.max(100, Math.round(contentHeight)) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse font size from a CSS font shorthand string like `400 32px "Font", sans-serif`.
|
|
66
|
+
* Returns the numeric pixel value, or null if not found.
|
|
67
|
+
*/
|
|
68
|
+
function parseFontSize(font: string): number | null {
|
|
69
|
+
const m = font.match(/\b(\d+(?:\.\d+)?)px\b/);
|
|
70
|
+
return m ? parseFloat(m[1]!) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Return a new theme where every bodyStyle font size is capped at fontSizeMax (px).
|
|
75
|
+
* Line heights are scaled proportionally when the font size is reduced.
|
|
76
|
+
* Used alongside applyContentHeightOverride to prevent overly large text when
|
|
77
|
+
* per-page content area is small (few words forced onto a fixed number of pages).
|
|
78
|
+
*/
|
|
79
|
+
export function applyFontSizeMax(theme: LongformTheme, fontSizeMax: number): LongformTheme {
|
|
80
|
+
const clampedStyles = {} as Record<ContentBlockKind, LongformLineStyle>;
|
|
81
|
+
for (const [kind, style] of Object.entries(theme.bodyStyles) as [ContentBlockKind, LongformLineStyle][]) {
|
|
82
|
+
const original = parseFontSize(style.font);
|
|
83
|
+
if (original !== null && original > fontSizeMax) {
|
|
84
|
+
const ratio = fontSizeMax / original;
|
|
85
|
+
const newFont = style.font.replace(/\b\d+(?:\.\d+)?px\b/, `${fontSizeMax}px`);
|
|
86
|
+
const newLineHeight = Math.round(style.lineHeight * ratio);
|
|
87
|
+
clampedStyles[kind] = { ...style, font: newFont, lineHeight: newLineHeight };
|
|
88
|
+
} else {
|
|
89
|
+
clampedStyles[kind] = style;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ...theme, bodyStyles: clampedStyles };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getLongformGeometry(theme: LongformTheme): LongformGeometry {
|
|
96
|
+
const bodyWidth = Math.max(120, theme.pageWidth - theme.bodyPaddingX * 2);
|
|
97
|
+
const headerHeight = theme.logoSize + theme.logoGap;
|
|
98
|
+
const footerReservedHeight = theme.footerHeight + theme.footerMarginTop;
|
|
99
|
+
const derivedContentHeight = Math.max(
|
|
100
|
+
120,
|
|
101
|
+
theme.pageHeight - theme.bodyPaddingY * 2 - headerHeight - footerReservedHeight,
|
|
102
|
+
);
|
|
103
|
+
const contentWidth = Math.min(theme.contentWidth ?? bodyWidth, bodyWidth);
|
|
104
|
+
const contentHeight = Math.min(theme.contentHeight ?? derivedContentHeight, derivedContentHeight);
|
|
105
|
+
return {
|
|
106
|
+
pageWidth: theme.pageWidth,
|
|
107
|
+
pageHeight: theme.pageHeight,
|
|
108
|
+
bodyPaddingX: theme.bodyPaddingX,
|
|
109
|
+
bodyPaddingY: theme.bodyPaddingY,
|
|
110
|
+
logoSize: theme.logoSize,
|
|
111
|
+
logoGap: theme.logoGap,
|
|
112
|
+
footerMarginTop: theme.footerMarginTop,
|
|
113
|
+
footerHeight: theme.footerHeight,
|
|
114
|
+
contentBottomGap: theme.contentBottomGap,
|
|
115
|
+
contentWidth,
|
|
116
|
+
contentHeight,
|
|
117
|
+
contentStageHeight: Math.max(80, contentHeight - theme.contentBottomGap),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const BODY_FS = 32;
|
|
122
|
+
const HEADING_FS = 50; // 32 * 1.25^2
|
|
123
|
+
const QUOTE_FS = 30;
|
|
124
|
+
|
|
125
|
+
const BASE_THEME: Omit<LongformTheme, "name" | "bgColor" | "bodyColor" | "accentColor" | "quoteColor" | "captionColor" | "watermarkColor" | "watermarkOpacity"> = {
|
|
126
|
+
pageWidth: LONGFORM_PAGE_WIDTH,
|
|
127
|
+
pageHeight: LONGFORM_PAGE_HEIGHT,
|
|
128
|
+
imageRadius: 22,
|
|
129
|
+
|
|
130
|
+
// Proportional geometry: ~4.5% of page height for vertical padding
|
|
131
|
+
bodyPaddingX: Math.round(LONGFORM_PAGE_WIDTH * 0.10), // 90
|
|
132
|
+
bodyPaddingY: Math.round(LONGFORM_PAGE_HEIGHT * 0.045), // 54 (was 48)
|
|
133
|
+
logoSize: 72,
|
|
134
|
+
logoGap: Math.round(LONGFORM_PAGE_HEIGHT * 0.045 * 0.78), // ~42 (was 40)
|
|
135
|
+
footerMarginTop: Math.round(BODY_FS * 1.2), // 38 (was 28)
|
|
136
|
+
footerHeight: Math.round(BODY_FS * 1.0), // 32 (unchanged)
|
|
137
|
+
contentBottomGap: Math.round(BODY_FS * 0.6), // 19 (was 12)
|
|
138
|
+
|
|
139
|
+
contentWidth: null,
|
|
140
|
+
contentHeight: null,
|
|
141
|
+
|
|
142
|
+
bodyStyles: {
|
|
143
|
+
paragraph: {
|
|
144
|
+
font: `400 ${BODY_FS}px "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif`,
|
|
145
|
+
lineHeight: proportionalLineHeight(BODY_FS),
|
|
146
|
+
className: "body-line",
|
|
147
|
+
gapAfter: computeSpacing(BODY_FS, BODY_FS, 0.55),
|
|
148
|
+
},
|
|
149
|
+
heading: {
|
|
150
|
+
font: `700 ${HEADING_FS}px "AlimamaShuHeiTi", "PingFang SC", sans-serif`,
|
|
151
|
+
lineHeight: proportionalLineHeight(HEADING_FS),
|
|
152
|
+
className: "body-heading",
|
|
153
|
+
gapBefore: computeSpacing(BODY_FS, HEADING_FS, 0.85),
|
|
154
|
+
gapAfter: computeSpacing(HEADING_FS, BODY_FS, 0.45),
|
|
155
|
+
},
|
|
156
|
+
quote: {
|
|
157
|
+
font: `400 ${QUOTE_FS}px "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif`,
|
|
158
|
+
lineHeight: proportionalLineHeight(QUOTE_FS),
|
|
159
|
+
className: "body-quote",
|
|
160
|
+
gapBefore: computeSpacing(BODY_FS, QUOTE_FS, 0.6),
|
|
161
|
+
gapAfter: computeSpacing(QUOTE_FS, BODY_FS, 0.5),
|
|
162
|
+
},
|
|
163
|
+
"list-item": {
|
|
164
|
+
font: `400 ${BODY_FS}px "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif`,
|
|
165
|
+
lineHeight: proportionalLineHeight(BODY_FS),
|
|
166
|
+
className: "body-line",
|
|
167
|
+
gapAfter: computeSpacing(BODY_FS, BODY_FS, 0.3),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const THEMES: Record<string, LongformTheme> = {
|
|
173
|
+
"paper-sage": {
|
|
174
|
+
...BASE_THEME,
|
|
175
|
+
name: "paper-sage",
|
|
176
|
+
bgColor: "#f9fcfa",
|
|
177
|
+
bodyColor: "#22201d",
|
|
178
|
+
accentColor: "#1d4f39",
|
|
179
|
+
quoteColor: "#474038",
|
|
180
|
+
captionColor: "#6a6257",
|
|
181
|
+
watermarkColor: "#555555",
|
|
182
|
+
watermarkOpacity: 0.55,
|
|
183
|
+
},
|
|
184
|
+
"linen-news": {
|
|
185
|
+
...BASE_THEME,
|
|
186
|
+
name: "linen-news",
|
|
187
|
+
bgColor: "#f7f3eb",
|
|
188
|
+
bodyColor: "#2b2621",
|
|
189
|
+
accentColor: "#7a2e24",
|
|
190
|
+
quoteColor: "#625449",
|
|
191
|
+
captionColor: "#7a6a5d",
|
|
192
|
+
watermarkColor: "#6a6158",
|
|
193
|
+
watermarkOpacity: 0.62,
|
|
194
|
+
bodyStyles: {
|
|
195
|
+
...BASE_THEME.bodyStyles,
|
|
196
|
+
heading: {
|
|
197
|
+
...BASE_THEME.bodyStyles.heading,
|
|
198
|
+
font: `700 36px "AlimamaShuHeiTi", "PingFang SC", sans-serif`,
|
|
199
|
+
lineHeight: proportionalLineHeight(36),
|
|
200
|
+
gapBefore: computeSpacing(BODY_FS, 36, 0.85),
|
|
201
|
+
gapAfter: computeSpacing(36, BODY_FS, 0.45),
|
|
202
|
+
},
|
|
203
|
+
paragraph: {
|
|
204
|
+
...BASE_THEME.bodyStyles.paragraph,
|
|
205
|
+
font: `400 31px "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif`,
|
|
206
|
+
lineHeight: 56,
|
|
207
|
+
},
|
|
208
|
+
quote: {
|
|
209
|
+
...BASE_THEME.bodyStyles.quote,
|
|
210
|
+
lineHeight: 54,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export function getLongformTheme(name: string): LongformTheme {
|
|
217
|
+
return THEMES[name] ?? THEMES["paper-sage"]!;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getLongformThemeCssVars(theme: LongformTheme): string {
|
|
221
|
+
const geometry = getLongformGeometry(theme);
|
|
222
|
+
return [
|
|
223
|
+
`--page-width:${theme.pageWidth}px`,
|
|
224
|
+
`--page-height:${theme.pageHeight}px`,
|
|
225
|
+
`--page-bg:${theme.bgColor}`,
|
|
226
|
+
`--body-color:${theme.bodyColor}`,
|
|
227
|
+
`--accent-color:${theme.accentColor}`,
|
|
228
|
+
`--quote-color:${theme.quoteColor}`,
|
|
229
|
+
`--caption-color:${theme.captionColor}`,
|
|
230
|
+
`--watermark-color:${theme.watermarkColor}`,
|
|
231
|
+
`--watermark-opacity:${theme.watermarkOpacity}`,
|
|
232
|
+
`--body-padding-x:${theme.bodyPaddingX}px`,
|
|
233
|
+
`--body-padding-y:${theme.bodyPaddingY}px`,
|
|
234
|
+
`--logo-size:${theme.logoSize}px`,
|
|
235
|
+
`--logo-gap:${theme.logoGap}px`,
|
|
236
|
+
`--footer-margin-top:${theme.footerMarginTop}px`,
|
|
237
|
+
`--footer-height:${theme.footerHeight}px`,
|
|
238
|
+
`--content-width:${geometry.contentWidth}px`,
|
|
239
|
+
`--content-height:${geometry.contentHeight}px`,
|
|
240
|
+
`--content-bottom-gap:${theme.contentBottomGap}px`,
|
|
241
|
+
`--image-radius:${theme.imageRadius}px`,
|
|
242
|
+
].join(";");
|
|
243
|
+
}
|