@akairos/typeset-studio 0.2.1 → 0.2.3
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 +91 -95
- package/dist/assets/index-3Ko1UCBU.css +1 -0
- package/dist/assets/index-C9J0f4Lf.js +317 -0
- package/dist/index.html +2 -2
- package/dist/lib/adapter.js +6 -7734
- package/dist/lib/canvas-renderer-cxnar75n.js +23 -0
- package/dist/lib/font-loader-j28mt43t.js +11 -0
- package/dist/lib/index-0370qz9e.js +303 -0
- package/dist/lib/index-37x76zdn.js +4 -0
- package/dist/lib/index-6c43c7h4.js +3880 -0
- package/dist/lib/index-t9w3p2p9.js +3366 -0
- package/dist/lib/index-zdhqg5f0.js +459 -0
- package/dist/lib/index.js +26 -7734
- package/dist/lib/render-xxd348m1.js +11 -0
- package/package.json +3 -7
- package/src/adapter.test.ts +117 -0
- package/src/adapter.ts +190 -281
- package/src/core/canvas-draw.ts +150 -47
- package/src/core/canvas-renderer.test.ts +58 -0
- package/src/core/canvas-renderer.ts +2 -1
- package/src/core/data-dir.ts +13 -0
- package/src/core/engine.ts +86 -56
- package/src/core/font-loader.test.ts +20 -0
- package/src/core/font-loader.ts +6 -2
- package/src/core/image-layout.ts +4 -0
- package/src/core/inline-text.ts +6 -4
- package/src/core/obstacle-flow.ts +29 -15
- package/src/core/page-images.test.ts +90 -0
- package/src/core/pretext.ts +4 -10
- package/src/core/template.test.ts +38 -1
- package/src/core/template.ts +53 -41
- package/src/core/types.ts +46 -0
- package/src/core/typography.ts +0 -14
- package/src/index.ts +6 -3
- package/src/server/auth.ts +18 -13
- package/src/server/index.ts +11 -16
- package/src/server/routes/fonts.ts +1 -1
- package/src/server/routes/render.ts +60 -17
- package/src/server/routes/slots.test.ts +96 -0
- package/src/server/routes/templates.ts +37 -48
- package/src/server/routes/upload.ts +2 -1
- package/src/web/App.tsx +15 -6
- package/src/web/components/CanvasViewport.tsx +56 -42
- package/src/web/components/EditorSidebar.tsx +59 -48
- package/src/web/components/Header.tsx +1 -1
- package/src/web/components/TemplateModal.tsx +25 -28
- package/src/web/hooks/image-layout.ts +31 -0
- package/src/web/hooks/useLocalFonts.ts +10 -1
- package/src/web/hooks/useTypeset.ts +139 -100
- package/data/templates/.gitkeep +0 -0
- package/data/templates/tpl_1791681833354_iofsk.json +0 -46
- package/data/templates/tpl_1791682603199_875ny.json +0 -66
- package/data/templates/tpl_1791682780929_qnuiy.json +0 -66
- package/data/templates/tpl_longform_paper_sage.json +0 -47
- package/data/templates/tpl_longform_vintage_news.json +0 -47
- package/data/templates/tpl_newspic_poster_cover.json +0 -64
- package/data/templates/tpl_square_quote_card.json +0 -46
- package/data/templates/tpl_wechat_cover_editorial.json +0 -64
- package/data/templates/tpl_wechat_cover_minimal.json +0 -64
- package/data/templates/tpl_wide_tech_banner.json +0 -46
- package/dist/assets/index-CZ352D7X.css +0 -1
- package/dist/assets/index-Dpnz-F2g.js +0 -317
|
@@ -0,0 +1,3366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeStageDimensions,
|
|
3
|
+
getCanvasSpec,
|
|
4
|
+
renderPageToPng,
|
|
5
|
+
renderTypesetResultToPngs,
|
|
6
|
+
resolveDataDir
|
|
7
|
+
} from "./index-zdhqg5f0.js";
|
|
8
|
+
import {
|
|
9
|
+
buildLineTextFromRange,
|
|
10
|
+
getLineTextCache,
|
|
11
|
+
measureNaturalWidth,
|
|
12
|
+
prepareWithSegments,
|
|
13
|
+
stepPreparedLineGeometry
|
|
14
|
+
} from "./index-6c43c7h4.js";
|
|
15
|
+
import {
|
|
16
|
+
__require
|
|
17
|
+
} from "./index-37x76zdn.js";
|
|
18
|
+
|
|
19
|
+
// node_modules/hono/dist/request/constants.js
|
|
20
|
+
var GET_MATCH_RESULT = Symbol();
|
|
21
|
+
|
|
22
|
+
// node_modules/hono/dist/utils/buffer.js
|
|
23
|
+
var bufferToFormData = (arrayBuffer, contentType) => {
|
|
24
|
+
return new Response(arrayBuffer, { headers: { "Content-Type": contentType.replace(/^[^;]+/, (mediaType) => mediaType.toLowerCase()) } }).formData();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// node_modules/hono/dist/utils/body.js
|
|
28
|
+
var MAX_NESTED_OBJECTS = 1e4;
|
|
29
|
+
var isRawRequest = (request) => ("headers" in request);
|
|
30
|
+
var parseBody = async (request, options = Object.create(null)) => {
|
|
31
|
+
const { all = false, dot = false } = options;
|
|
32
|
+
const mediaType = (isRawRequest(request) ? request.headers : request.raw.headers).get("Content-Type")?.split(";")[0].trim().toLowerCase();
|
|
33
|
+
if (mediaType === "multipart/form-data" || mediaType === "application/x-www-form-urlencoded")
|
|
34
|
+
return parseFormData(request, {
|
|
35
|
+
all,
|
|
36
|
+
dot
|
|
37
|
+
});
|
|
38
|
+
return {};
|
|
39
|
+
};
|
|
40
|
+
async function parseFormData(request, options) {
|
|
41
|
+
if (!isRawRequest(request) && request.bodyCache.formData)
|
|
42
|
+
return convertFormDataToBodyData(await request.bodyCache.formData, options);
|
|
43
|
+
const headers = isRawRequest(request) ? request.headers : request.raw.headers;
|
|
44
|
+
const arrayBuffer = await request.arrayBuffer();
|
|
45
|
+
const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || "");
|
|
46
|
+
if (!isRawRequest(request))
|
|
47
|
+
request.bodyCache.formData = formDataPromise;
|
|
48
|
+
const formData = await formDataPromise;
|
|
49
|
+
if (formData)
|
|
50
|
+
return convertFormDataToBodyData(formData, options);
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
function convertFormDataToBodyData(formData, options) {
|
|
54
|
+
const form = Object.create(null);
|
|
55
|
+
const nestingState = { count: 0 };
|
|
56
|
+
formData.forEach((value, key) => {
|
|
57
|
+
if (!(options.all || key.endsWith("[]")))
|
|
58
|
+
form[key] = value;
|
|
59
|
+
else
|
|
60
|
+
handleParsingAllValues(form, key, value);
|
|
61
|
+
});
|
|
62
|
+
if (options.dot)
|
|
63
|
+
Object.entries(form).forEach(([key, value]) => {
|
|
64
|
+
if (key.includes(".")) {
|
|
65
|
+
handleParsingNestedValues(form, key, value, nestingState);
|
|
66
|
+
delete form[key];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return form;
|
|
70
|
+
}
|
|
71
|
+
var handleParsingAllValues = (form, key, value) => {
|
|
72
|
+
if (form[key] !== undefined) {
|
|
73
|
+
if (Array.isArray(form[key]))
|
|
74
|
+
form[key].push(value);
|
|
75
|
+
else
|
|
76
|
+
form[key] = [form[key], value];
|
|
77
|
+
} else if (!key.endsWith("[]"))
|
|
78
|
+
form[key] = value;
|
|
79
|
+
else
|
|
80
|
+
form[key] = [value];
|
|
81
|
+
};
|
|
82
|
+
var handleParsingNestedValues = (form, key, value, state) => {
|
|
83
|
+
if (/(?:^|\.)__proto__\./.test(key))
|
|
84
|
+
return;
|
|
85
|
+
let nestedForm = form;
|
|
86
|
+
const keys = key.split(".", 34);
|
|
87
|
+
if (keys.length > 33)
|
|
88
|
+
throwNestingLimitExceeded();
|
|
89
|
+
keys.forEach((key2, index) => {
|
|
90
|
+
if (index === keys.length - 1)
|
|
91
|
+
nestedForm[key2] = value;
|
|
92
|
+
else {
|
|
93
|
+
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
94
|
+
if (state.count++ >= MAX_NESTED_OBJECTS)
|
|
95
|
+
throwNestingLimitExceeded();
|
|
96
|
+
nestedForm[key2] = Object.create(null);
|
|
97
|
+
}
|
|
98
|
+
nestedForm = nestedForm[key2];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
var throwNestingLimitExceeded = () => {
|
|
103
|
+
throw new Error("Nesting limit exceeded");
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// node_modules/hono/dist/utils/url.js
|
|
107
|
+
var splitPath = (path) => {
|
|
108
|
+
const paths = path.split("/");
|
|
109
|
+
if (paths[0] === "")
|
|
110
|
+
paths.shift();
|
|
111
|
+
return paths;
|
|
112
|
+
};
|
|
113
|
+
var splitRoutingPath = (routePath) => {
|
|
114
|
+
const { groups, path } = extractGroupsFromPath(routePath);
|
|
115
|
+
const paths = splitPath(path);
|
|
116
|
+
return replaceGroupMarks(paths, groups);
|
|
117
|
+
};
|
|
118
|
+
var extractGroupsFromPath = (path) => {
|
|
119
|
+
const groups = [];
|
|
120
|
+
path = path.replace(/\{[^}]+\}/g, (match, index) => {
|
|
121
|
+
const mark = `@${index}`;
|
|
122
|
+
groups.push([mark, match]);
|
|
123
|
+
return mark;
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
groups,
|
|
127
|
+
path
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
var replaceGroupMarks = (paths, groups) => {
|
|
131
|
+
for (let i = groups.length - 1;i >= 0; i--) {
|
|
132
|
+
const [mark] = groups[i];
|
|
133
|
+
for (let j = paths.length - 1;j >= 0; j--)
|
|
134
|
+
if (paths[j].includes(mark)) {
|
|
135
|
+
paths[j] = paths[j].replace(mark, groups[i][1]);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return paths;
|
|
140
|
+
};
|
|
141
|
+
var patternCache = {};
|
|
142
|
+
var getPattern = (label, next) => {
|
|
143
|
+
if (label === "*")
|
|
144
|
+
return "*";
|
|
145
|
+
const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
146
|
+
if (match) {
|
|
147
|
+
const cacheKey = `${label}#${next}`;
|
|
148
|
+
if (!patternCache[cacheKey]) {
|
|
149
|
+
if (match[2])
|
|
150
|
+
patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [
|
|
151
|
+
cacheKey,
|
|
152
|
+
match[1],
|
|
153
|
+
new RegExp(`^${match[2]}(?=/${next})`)
|
|
154
|
+
] : [
|
|
155
|
+
label,
|
|
156
|
+
match[1],
|
|
157
|
+
new RegExp(`^${match[2]}$`)
|
|
158
|
+
];
|
|
159
|
+
else
|
|
160
|
+
patternCache[cacheKey] = [
|
|
161
|
+
label,
|
|
162
|
+
match[1],
|
|
163
|
+
true
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
return patternCache[cacheKey];
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
var tryDecode = (str, decoder) => {
|
|
171
|
+
try {
|
|
172
|
+
return decoder(str);
|
|
173
|
+
} catch {
|
|
174
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
|
|
175
|
+
try {
|
|
176
|
+
return decoder(match);
|
|
177
|
+
} catch {
|
|
178
|
+
return match;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var tryDecodeURI = (str) => tryDecode(str, decodeURI);
|
|
184
|
+
var getPath = (request) => {
|
|
185
|
+
const url = request.url;
|
|
186
|
+
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
187
|
+
let i = start;
|
|
188
|
+
for (;i < url.length; i++) {
|
|
189
|
+
const charCode = url.charCodeAt(i);
|
|
190
|
+
if (charCode === 37) {
|
|
191
|
+
const queryIndex = url.indexOf("?", i);
|
|
192
|
+
const hashIndex = url.indexOf("#", i);
|
|
193
|
+
const end = queryIndex === -1 ? hashIndex === -1 ? undefined : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
194
|
+
const path = url.slice(start, end);
|
|
195
|
+
return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
|
|
196
|
+
} else if (charCode === 63 || charCode === 35)
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
return url.slice(start, i);
|
|
200
|
+
};
|
|
201
|
+
var getPathNoStrict = (request) => {
|
|
202
|
+
const result = getPath(request);
|
|
203
|
+
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
|
|
204
|
+
};
|
|
205
|
+
var mergePath = (base, sub, ...rest) => {
|
|
206
|
+
if (rest.length)
|
|
207
|
+
sub = mergePath(sub, ...rest);
|
|
208
|
+
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
|
|
209
|
+
};
|
|
210
|
+
var checkOptionalParameter = (path) => {
|
|
211
|
+
if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":"))
|
|
212
|
+
return null;
|
|
213
|
+
const segments = path.split("/");
|
|
214
|
+
const results = [];
|
|
215
|
+
let basePath = "";
|
|
216
|
+
segments.forEach((segment) => {
|
|
217
|
+
if (segment !== "" && !/\:/.test(segment))
|
|
218
|
+
basePath += "/" + segment;
|
|
219
|
+
else if (/\:/.test(segment)) {
|
|
220
|
+
if (segment.charCodeAt(segment.length - 1) === 63) {
|
|
221
|
+
if (results.length === 0 && basePath === "")
|
|
222
|
+
results.push("/");
|
|
223
|
+
else
|
|
224
|
+
results.push(basePath);
|
|
225
|
+
const optionalSegment = segment.slice(0, -1);
|
|
226
|
+
basePath += "/" + optionalSegment;
|
|
227
|
+
results.push(basePath);
|
|
228
|
+
} else
|
|
229
|
+
basePath += "/" + segment;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return results.filter((v, i, a) => a.indexOf(v) === i);
|
|
233
|
+
};
|
|
234
|
+
var tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str;
|
|
235
|
+
var _decodeURI = (value) => {
|
|
236
|
+
if (value.indexOf("+") !== -1)
|
|
237
|
+
value = value.replace(/\+/g, " ");
|
|
238
|
+
return tryDecodeURIComponent(value);
|
|
239
|
+
};
|
|
240
|
+
var _getQueryParam = (url, key, multiple) => {
|
|
241
|
+
const hashIndex = url.indexOf("#", 8);
|
|
242
|
+
if (hashIndex !== -1)
|
|
243
|
+
url = url.slice(0, hashIndex);
|
|
244
|
+
let encoded;
|
|
245
|
+
if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
|
|
246
|
+
let keyIndex2 = url.indexOf("?", 8);
|
|
247
|
+
if (keyIndex2 === -1)
|
|
248
|
+
return;
|
|
249
|
+
if (!url.startsWith(key, keyIndex2 + 1))
|
|
250
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
251
|
+
while (keyIndex2 !== -1) {
|
|
252
|
+
const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
|
|
253
|
+
if (trailingKeyCode === 61) {
|
|
254
|
+
const valueIndex = keyIndex2 + key.length + 2;
|
|
255
|
+
const endIndex = url.indexOf("&", valueIndex);
|
|
256
|
+
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? undefined : endIndex));
|
|
257
|
+
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode))
|
|
258
|
+
return "";
|
|
259
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
260
|
+
}
|
|
261
|
+
encoded = /[%+]/.test(url);
|
|
262
|
+
if (!encoded)
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const results = Object.create(null);
|
|
266
|
+
encoded ??= /[%+]/.test(url);
|
|
267
|
+
let keyIndex = url.indexOf("?", 8);
|
|
268
|
+
while (keyIndex !== -1) {
|
|
269
|
+
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
|
|
270
|
+
let valueIndex = url.indexOf("=", keyIndex);
|
|
271
|
+
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1)
|
|
272
|
+
valueIndex = -1;
|
|
273
|
+
let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? undefined : nextKeyIndex : valueIndex);
|
|
274
|
+
if (encoded)
|
|
275
|
+
name = _decodeURI(name);
|
|
276
|
+
keyIndex = nextKeyIndex;
|
|
277
|
+
if (name === "")
|
|
278
|
+
continue;
|
|
279
|
+
let value;
|
|
280
|
+
if (valueIndex === -1)
|
|
281
|
+
value = "";
|
|
282
|
+
else {
|
|
283
|
+
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? undefined : nextKeyIndex);
|
|
284
|
+
if (encoded)
|
|
285
|
+
value = _decodeURI(value);
|
|
286
|
+
}
|
|
287
|
+
if (multiple) {
|
|
288
|
+
if (!(results[name] && Array.isArray(results[name])))
|
|
289
|
+
results[name] = [];
|
|
290
|
+
results[name].push(value);
|
|
291
|
+
} else
|
|
292
|
+
results[name] ??= value;
|
|
293
|
+
}
|
|
294
|
+
return key ? results[key] : results;
|
|
295
|
+
};
|
|
296
|
+
var getQueryParam = _getQueryParam;
|
|
297
|
+
var getQueryParams = (url, key) => {
|
|
298
|
+
return _getQueryParam(url, key, true);
|
|
299
|
+
};
|
|
300
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
301
|
+
|
|
302
|
+
// node_modules/hono/dist/request.js
|
|
303
|
+
var HonoRequest = class {
|
|
304
|
+
raw;
|
|
305
|
+
#validatedData;
|
|
306
|
+
#matchResult;
|
|
307
|
+
routeIndex = 0;
|
|
308
|
+
path;
|
|
309
|
+
bodyCache = {};
|
|
310
|
+
constructor(request, path = "/", matchResult = [[]]) {
|
|
311
|
+
this.raw = request;
|
|
312
|
+
this.path = path;
|
|
313
|
+
this.#matchResult = matchResult;
|
|
314
|
+
}
|
|
315
|
+
param(key) {
|
|
316
|
+
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
|
|
317
|
+
}
|
|
318
|
+
#getDecodedParam(key) {
|
|
319
|
+
const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key];
|
|
320
|
+
const param = this.#getParamValue(paramKey);
|
|
321
|
+
return param && tryDecodeURIComponent(param);
|
|
322
|
+
}
|
|
323
|
+
#getAllDecodedParams() {
|
|
324
|
+
const decoded = {};
|
|
325
|
+
const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {});
|
|
326
|
+
for (const key of keys) {
|
|
327
|
+
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
|
|
328
|
+
if (value !== undefined)
|
|
329
|
+
decoded[key] = tryDecodeURIComponent(value);
|
|
330
|
+
}
|
|
331
|
+
return decoded;
|
|
332
|
+
}
|
|
333
|
+
#getParamValue(paramKey) {
|
|
334
|
+
return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
|
|
335
|
+
}
|
|
336
|
+
query(key) {
|
|
337
|
+
return getQueryParam(this.url, key);
|
|
338
|
+
}
|
|
339
|
+
queries(key) {
|
|
340
|
+
return getQueryParams(this.url, key);
|
|
341
|
+
}
|
|
342
|
+
header(name) {
|
|
343
|
+
if (name)
|
|
344
|
+
return this.raw.headers.get(name) ?? undefined;
|
|
345
|
+
const headerData = Object.create(null);
|
|
346
|
+
this.raw.headers.forEach((value, key) => {
|
|
347
|
+
headerData[key] = value;
|
|
348
|
+
});
|
|
349
|
+
return headerData;
|
|
350
|
+
}
|
|
351
|
+
async parseBody(options) {
|
|
352
|
+
return parseBody(this, options);
|
|
353
|
+
}
|
|
354
|
+
#cachedBody = (key) => {
|
|
355
|
+
const { bodyCache, raw } = this;
|
|
356
|
+
const cachedBody = bodyCache[key];
|
|
357
|
+
if (cachedBody)
|
|
358
|
+
return cachedBody;
|
|
359
|
+
for (const anyCachedKey in bodyCache)
|
|
360
|
+
return bodyCache[anyCachedKey].then((body) => {
|
|
361
|
+
if (anyCachedKey === "json")
|
|
362
|
+
body = JSON.stringify(body);
|
|
363
|
+
const contentType = anyCachedKey === "formData" ? undefined : raw.headers.get("content-type");
|
|
364
|
+
return new Response(body, { headers: contentType ? { "Content-Type": contentType } : undefined })[key]();
|
|
365
|
+
});
|
|
366
|
+
return bodyCache[key] = raw[key]();
|
|
367
|
+
};
|
|
368
|
+
json() {
|
|
369
|
+
return this.#cachedBody("text").then((text) => JSON.parse(text));
|
|
370
|
+
}
|
|
371
|
+
text() {
|
|
372
|
+
return this.#cachedBody("text");
|
|
373
|
+
}
|
|
374
|
+
arrayBuffer() {
|
|
375
|
+
return this.#cachedBody("arrayBuffer");
|
|
376
|
+
}
|
|
377
|
+
bytes() {
|
|
378
|
+
return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
|
|
379
|
+
}
|
|
380
|
+
blob() {
|
|
381
|
+
return this.#cachedBody("blob");
|
|
382
|
+
}
|
|
383
|
+
formData() {
|
|
384
|
+
return this.#cachedBody("formData");
|
|
385
|
+
}
|
|
386
|
+
addValidatedData(target, data) {
|
|
387
|
+
(this.#validatedData ??= {})[target] = data;
|
|
388
|
+
}
|
|
389
|
+
valid(target) {
|
|
390
|
+
return this.#validatedData?.[target];
|
|
391
|
+
}
|
|
392
|
+
get url() {
|
|
393
|
+
return this.raw.url;
|
|
394
|
+
}
|
|
395
|
+
get method() {
|
|
396
|
+
return this.raw.method;
|
|
397
|
+
}
|
|
398
|
+
get [GET_MATCH_RESULT]() {
|
|
399
|
+
return this.#matchResult;
|
|
400
|
+
}
|
|
401
|
+
get matchedRoutes() {
|
|
402
|
+
return this.#matchResult[0].map(([[, route]]) => route);
|
|
403
|
+
}
|
|
404
|
+
get routePath() {
|
|
405
|
+
return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// node_modules/hono/dist/utils/html.js
|
|
410
|
+
var HtmlEscapedCallbackPhase = {
|
|
411
|
+
Stringify: 1,
|
|
412
|
+
BeforeStream: 2,
|
|
413
|
+
Stream: 3
|
|
414
|
+
};
|
|
415
|
+
var raw = (value, callbacks) => {
|
|
416
|
+
const escapedString = new String(value);
|
|
417
|
+
escapedString.isEscaped = true;
|
|
418
|
+
escapedString.callbacks = callbacks;
|
|
419
|
+
return escapedString;
|
|
420
|
+
};
|
|
421
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
|
|
422
|
+
if (typeof str === "object" && !(str instanceof String)) {
|
|
423
|
+
if (!(str instanceof Promise))
|
|
424
|
+
str = str.toString();
|
|
425
|
+
if (str instanceof Promise)
|
|
426
|
+
str = await str;
|
|
427
|
+
}
|
|
428
|
+
const callbacks = str.callbacks;
|
|
429
|
+
if (!callbacks?.length)
|
|
430
|
+
return Promise.resolve(str);
|
|
431
|
+
if (buffer)
|
|
432
|
+
buffer[0] += str;
|
|
433
|
+
else
|
|
434
|
+
buffer = [str];
|
|
435
|
+
const resStr = Promise.all(callbacks.map((c) => c({
|
|
436
|
+
phase,
|
|
437
|
+
buffer,
|
|
438
|
+
context
|
|
439
|
+
}))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))).then(() => buffer[0]));
|
|
440
|
+
if (preserveCallbacks)
|
|
441
|
+
return raw(await resStr, callbacks);
|
|
442
|
+
else
|
|
443
|
+
return resStr;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// node_modules/hono/dist/context.js
|
|
447
|
+
var TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
448
|
+
var setDefaultContentType = (contentType, headers) => {
|
|
449
|
+
return {
|
|
450
|
+
"Content-Type": contentType,
|
|
451
|
+
...headers
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
var createResponseInstance = (body, init) => new Response(body, init);
|
|
455
|
+
var Context = class {
|
|
456
|
+
#rawRequest;
|
|
457
|
+
#req;
|
|
458
|
+
env = {};
|
|
459
|
+
#var;
|
|
460
|
+
finalized = false;
|
|
461
|
+
error;
|
|
462
|
+
#status;
|
|
463
|
+
#executionCtx;
|
|
464
|
+
#res;
|
|
465
|
+
#layout;
|
|
466
|
+
#renderer;
|
|
467
|
+
#notFoundHandler;
|
|
468
|
+
#preparedHeaders;
|
|
469
|
+
#matchResult;
|
|
470
|
+
#path;
|
|
471
|
+
constructor(req, options) {
|
|
472
|
+
this.#rawRequest = req;
|
|
473
|
+
if (options) {
|
|
474
|
+
this.#executionCtx = options.executionCtx;
|
|
475
|
+
this.env = options.env;
|
|
476
|
+
this.#notFoundHandler = options.notFoundHandler;
|
|
477
|
+
this.#path = options.path;
|
|
478
|
+
this.#matchResult = options.matchResult;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
get req() {
|
|
482
|
+
this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
|
|
483
|
+
return this.#req;
|
|
484
|
+
}
|
|
485
|
+
get event() {
|
|
486
|
+
if (this.#executionCtx && "respondWith" in this.#executionCtx)
|
|
487
|
+
return this.#executionCtx;
|
|
488
|
+
else
|
|
489
|
+
throw Error("This context has no FetchEvent");
|
|
490
|
+
}
|
|
491
|
+
get executionCtx() {
|
|
492
|
+
if (this.#executionCtx)
|
|
493
|
+
return this.#executionCtx;
|
|
494
|
+
else
|
|
495
|
+
throw Error("This context has no ExecutionContext");
|
|
496
|
+
}
|
|
497
|
+
get res() {
|
|
498
|
+
return this.#res ||= createResponseInstance(null, { headers: this.#preparedHeaders ??= new Headers });
|
|
499
|
+
}
|
|
500
|
+
set res(_res) {
|
|
501
|
+
if (this.#res && _res) {
|
|
502
|
+
_res = createResponseInstance(_res.body, _res);
|
|
503
|
+
for (const [k, v] of this.#res.headers.entries()) {
|
|
504
|
+
if (k === "content-type")
|
|
505
|
+
continue;
|
|
506
|
+
if (k === "set-cookie") {
|
|
507
|
+
const cookies = this.#res.headers.getSetCookie();
|
|
508
|
+
_res.headers.delete("set-cookie");
|
|
509
|
+
for (const cookie of cookies)
|
|
510
|
+
_res.headers.append("set-cookie", cookie);
|
|
511
|
+
} else
|
|
512
|
+
_res.headers.set(k, v);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
this.#res = _res;
|
|
516
|
+
this.finalized = true;
|
|
517
|
+
}
|
|
518
|
+
render = (...args) => {
|
|
519
|
+
this.#renderer ??= (content) => this.html(content);
|
|
520
|
+
return this.#renderer(...args);
|
|
521
|
+
};
|
|
522
|
+
setLayout = (layout) => this.#layout = layout;
|
|
523
|
+
getLayout = () => this.#layout;
|
|
524
|
+
setRenderer = (renderer) => {
|
|
525
|
+
this.#renderer = renderer;
|
|
526
|
+
};
|
|
527
|
+
header = (name, value, options) => {
|
|
528
|
+
if (this.finalized)
|
|
529
|
+
this.#res = createResponseInstance(this.#res.body, this.#res);
|
|
530
|
+
const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers;
|
|
531
|
+
if (value === undefined)
|
|
532
|
+
headers.delete(name);
|
|
533
|
+
else if (options?.append)
|
|
534
|
+
headers.append(name, value);
|
|
535
|
+
else
|
|
536
|
+
headers.set(name, value);
|
|
537
|
+
};
|
|
538
|
+
status = (status) => {
|
|
539
|
+
this.#status = status;
|
|
540
|
+
};
|
|
541
|
+
set = (key, value) => {
|
|
542
|
+
this.#var ??= /* @__PURE__ */ new Map;
|
|
543
|
+
this.#var.set(key, value);
|
|
544
|
+
};
|
|
545
|
+
get = (key) => {
|
|
546
|
+
return this.#var ? this.#var.get(key) : undefined;
|
|
547
|
+
};
|
|
548
|
+
get var() {
|
|
549
|
+
if (!this.#var)
|
|
550
|
+
return {};
|
|
551
|
+
return Object.fromEntries(this.#var);
|
|
552
|
+
}
|
|
553
|
+
#newResponse(data, arg, headers) {
|
|
554
|
+
let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders;
|
|
555
|
+
if (typeof arg === "object" && arg.headers) {
|
|
556
|
+
responseHeaders ??= new Headers;
|
|
557
|
+
for (const [key, value] of new Headers(arg.headers))
|
|
558
|
+
if (key === "set-cookie")
|
|
559
|
+
responseHeaders.append(key, value);
|
|
560
|
+
else
|
|
561
|
+
responseHeaders.set(key, value);
|
|
562
|
+
}
|
|
563
|
+
if (headers) {
|
|
564
|
+
if (!responseHeaders) {
|
|
565
|
+
let count = 0;
|
|
566
|
+
for (const k in headers)
|
|
567
|
+
if (++count > 1 || typeof headers[k] !== "string") {
|
|
568
|
+
responseHeaders = new Headers;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (responseHeaders)
|
|
573
|
+
for (const k in headers) {
|
|
574
|
+
const v = headers[k];
|
|
575
|
+
if (typeof v === "string")
|
|
576
|
+
responseHeaders.set(k, v);
|
|
577
|
+
else {
|
|
578
|
+
responseHeaders.delete(k);
|
|
579
|
+
for (const v2 of v)
|
|
580
|
+
responseHeaders.append(k, v2);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
|
|
585
|
+
return createResponseInstance(data, {
|
|
586
|
+
status,
|
|
587
|
+
headers: responseHeaders ?? headers
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
newResponse = (...args) => this.#newResponse(...args);
|
|
591
|
+
body = (data, arg, headers) => this.#newResponse(data, arg, headers);
|
|
592
|
+
text = (text, arg, headers) => {
|
|
593
|
+
return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers));
|
|
594
|
+
};
|
|
595
|
+
json = (object, arg, headers) => {
|
|
596
|
+
return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers));
|
|
597
|
+
};
|
|
598
|
+
html = (html, arg, headers) => {
|
|
599
|
+
const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
|
|
600
|
+
return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
|
|
601
|
+
};
|
|
602
|
+
redirect = (location, status) => {
|
|
603
|
+
const locationString = String(location);
|
|
604
|
+
this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString));
|
|
605
|
+
return this.newResponse(null, status ?? 302);
|
|
606
|
+
};
|
|
607
|
+
notFound = () => {
|
|
608
|
+
this.#notFoundHandler ??= () => createResponseInstance();
|
|
609
|
+
return this.#notFoundHandler(this);
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// node_modules/hono/dist/compose.js
|
|
614
|
+
var compose = (middleware, onError, onNotFound) => {
|
|
615
|
+
return (context, next) => {
|
|
616
|
+
let index = -1;
|
|
617
|
+
return dispatch(0);
|
|
618
|
+
async function dispatch(i) {
|
|
619
|
+
if (i <= index)
|
|
620
|
+
throw new Error("next() called multiple times");
|
|
621
|
+
index = i;
|
|
622
|
+
let res;
|
|
623
|
+
let isError = false;
|
|
624
|
+
let handler;
|
|
625
|
+
if (middleware[i]) {
|
|
626
|
+
handler = middleware[i][0][0];
|
|
627
|
+
context.req.routeIndex = i;
|
|
628
|
+
} else
|
|
629
|
+
handler = i === middleware.length && next || undefined;
|
|
630
|
+
if (handler)
|
|
631
|
+
try {
|
|
632
|
+
res = await handler(context, () => dispatch(i + 1));
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (err instanceof Error && onError) {
|
|
635
|
+
context.error = err;
|
|
636
|
+
res = await onError(err, context);
|
|
637
|
+
isError = true;
|
|
638
|
+
} else
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
else if (context.finalized === false && onNotFound)
|
|
642
|
+
res = await onNotFound(context);
|
|
643
|
+
if (res && (context.finalized === false || isError))
|
|
644
|
+
context.res = res;
|
|
645
|
+
return context;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// node_modules/hono/dist/router.js
|
|
651
|
+
var METHODS = [
|
|
652
|
+
"get",
|
|
653
|
+
"post",
|
|
654
|
+
"put",
|
|
655
|
+
"delete",
|
|
656
|
+
"options",
|
|
657
|
+
"patch",
|
|
658
|
+
"query"
|
|
659
|
+
];
|
|
660
|
+
var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built.";
|
|
661
|
+
var UnsupportedPathError = class extends Error {
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// node_modules/hono/dist/utils/constants.js
|
|
665
|
+
var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
|
|
666
|
+
|
|
667
|
+
// node_modules/hono/dist/hono-base.js
|
|
668
|
+
var notFoundHandler = (c) => {
|
|
669
|
+
return c.text("404 Not Found", 404);
|
|
670
|
+
};
|
|
671
|
+
var errorHandler = (err, c) => {
|
|
672
|
+
if ("getResponse" in err) {
|
|
673
|
+
const res = err.getResponse();
|
|
674
|
+
return c.newResponse(res.body, res);
|
|
675
|
+
}
|
|
676
|
+
console.error(err);
|
|
677
|
+
return c.text("Internal Server Error", 500);
|
|
678
|
+
};
|
|
679
|
+
var Hono = class Hono2 {
|
|
680
|
+
get;
|
|
681
|
+
post;
|
|
682
|
+
put;
|
|
683
|
+
delete;
|
|
684
|
+
options;
|
|
685
|
+
patch;
|
|
686
|
+
query;
|
|
687
|
+
all;
|
|
688
|
+
on;
|
|
689
|
+
use;
|
|
690
|
+
router;
|
|
691
|
+
getPath;
|
|
692
|
+
_basePath = "/";
|
|
693
|
+
#path = "/";
|
|
694
|
+
routes = [];
|
|
695
|
+
constructor(options = {}) {
|
|
696
|
+
[...METHODS, "all"].forEach((method) => {
|
|
697
|
+
this[method] = (args1, ...args) => {
|
|
698
|
+
const methodName = method.toUpperCase();
|
|
699
|
+
if (typeof args1 === "string")
|
|
700
|
+
this.#path = args1;
|
|
701
|
+
else
|
|
702
|
+
this.#addRoute(methodName, this.#path, args1);
|
|
703
|
+
args.forEach((handler) => {
|
|
704
|
+
this.#addRoute(methodName, this.#path, handler);
|
|
705
|
+
});
|
|
706
|
+
return this;
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
this.on = (method, path, ...handlers) => {
|
|
710
|
+
for (const p of [path].flat()) {
|
|
711
|
+
this.#path = p;
|
|
712
|
+
for (const m of [method].flat()) {
|
|
713
|
+
const methodName = m.toUpperCase();
|
|
714
|
+
for (const handler of handlers)
|
|
715
|
+
this.#addRoute(methodName, this.#path, handler);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return this;
|
|
719
|
+
};
|
|
720
|
+
this.use = (arg1, ...handlers) => {
|
|
721
|
+
if (typeof arg1 === "string")
|
|
722
|
+
this.#path = arg1;
|
|
723
|
+
else {
|
|
724
|
+
this.#path = "*";
|
|
725
|
+
handlers.unshift(arg1);
|
|
726
|
+
}
|
|
727
|
+
handlers.forEach((handler) => {
|
|
728
|
+
this.#addRoute("ALL", this.#path, handler);
|
|
729
|
+
});
|
|
730
|
+
return this;
|
|
731
|
+
};
|
|
732
|
+
const { strict, ...optionsWithoutStrict } = options;
|
|
733
|
+
Object.assign(this, optionsWithoutStrict);
|
|
734
|
+
this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
|
|
735
|
+
}
|
|
736
|
+
#clone() {
|
|
737
|
+
const clone = new Hono2({
|
|
738
|
+
router: this.router,
|
|
739
|
+
getPath: this.getPath
|
|
740
|
+
});
|
|
741
|
+
clone.errorHandler = this.errorHandler;
|
|
742
|
+
clone.#notFoundHandler = this.#notFoundHandler;
|
|
743
|
+
clone.routes = this.routes;
|
|
744
|
+
return clone;
|
|
745
|
+
}
|
|
746
|
+
#notFoundHandler = notFoundHandler;
|
|
747
|
+
errorHandler = errorHandler;
|
|
748
|
+
route(path, app) {
|
|
749
|
+
const subApp = this.basePath(path);
|
|
750
|
+
app.routes.map((r) => {
|
|
751
|
+
let handler;
|
|
752
|
+
if (app.errorHandler === errorHandler)
|
|
753
|
+
handler = r.handler;
|
|
754
|
+
else {
|
|
755
|
+
handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
|
|
756
|
+
handler[COMPOSED_HANDLER] = r.handler;
|
|
757
|
+
}
|
|
758
|
+
subApp.#addRoute(r.method, r.path, handler, r.basePath);
|
|
759
|
+
});
|
|
760
|
+
return this;
|
|
761
|
+
}
|
|
762
|
+
basePath(path) {
|
|
763
|
+
const subApp = this.#clone();
|
|
764
|
+
subApp._basePath = mergePath(this._basePath, path);
|
|
765
|
+
return subApp;
|
|
766
|
+
}
|
|
767
|
+
onError = (handler) => {
|
|
768
|
+
this.errorHandler = handler;
|
|
769
|
+
return this;
|
|
770
|
+
};
|
|
771
|
+
notFound = (handler) => {
|
|
772
|
+
this.#notFoundHandler = handler;
|
|
773
|
+
return this;
|
|
774
|
+
};
|
|
775
|
+
mount(path, applicationHandler, options) {
|
|
776
|
+
let replaceRequest;
|
|
777
|
+
let optionHandler;
|
|
778
|
+
if (options) {
|
|
779
|
+
if (typeof options === "function")
|
|
780
|
+
optionHandler = options;
|
|
781
|
+
else {
|
|
782
|
+
optionHandler = options.optionHandler;
|
|
783
|
+
if (options.replaceRequest === false)
|
|
784
|
+
replaceRequest = (request) => request;
|
|
785
|
+
else
|
|
786
|
+
replaceRequest = options.replaceRequest;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const getOptions = optionHandler ? (c) => {
|
|
790
|
+
const options2 = optionHandler(c);
|
|
791
|
+
return Array.isArray(options2) ? options2 : [options2];
|
|
792
|
+
} : (c) => {
|
|
793
|
+
let executionContext = undefined;
|
|
794
|
+
try {
|
|
795
|
+
executionContext = c.executionCtx;
|
|
796
|
+
} catch {}
|
|
797
|
+
return [c.env, executionContext];
|
|
798
|
+
};
|
|
799
|
+
replaceRequest ||= (() => {
|
|
800
|
+
const mergedPath = mergePath(this._basePath, path);
|
|
801
|
+
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
|
|
802
|
+
return (request) => {
|
|
803
|
+
const url = new URL(request.url);
|
|
804
|
+
url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
|
|
805
|
+
return new Request(url, request);
|
|
806
|
+
};
|
|
807
|
+
})();
|
|
808
|
+
const handler = async (c, next) => {
|
|
809
|
+
const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c));
|
|
810
|
+
if (res)
|
|
811
|
+
return res;
|
|
812
|
+
await next();
|
|
813
|
+
};
|
|
814
|
+
this.#addRoute("ALL", mergePath(path, "*"), handler);
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
#addRoute(method, path, handler, baseRoutePath) {
|
|
818
|
+
path = mergePath(this._basePath, path);
|
|
819
|
+
const r = {
|
|
820
|
+
basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
|
|
821
|
+
path,
|
|
822
|
+
method,
|
|
823
|
+
handler
|
|
824
|
+
};
|
|
825
|
+
this.router.add(method, path, [handler, r]);
|
|
826
|
+
this.routes.push(r);
|
|
827
|
+
}
|
|
828
|
+
#handleError(err, c) {
|
|
829
|
+
if (err instanceof Error)
|
|
830
|
+
return this.errorHandler(err, c);
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
#dispatch(request, executionCtx, env, method) {
|
|
834
|
+
if (method === "HEAD")
|
|
835
|
+
return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
|
|
836
|
+
const path = this.getPath(request, { env });
|
|
837
|
+
const matchResult = this.router.match(method, path);
|
|
838
|
+
const c = new Context(request, {
|
|
839
|
+
path,
|
|
840
|
+
matchResult,
|
|
841
|
+
env,
|
|
842
|
+
executionCtx,
|
|
843
|
+
notFoundHandler: this.#notFoundHandler
|
|
844
|
+
});
|
|
845
|
+
if (matchResult[0].length === 1) {
|
|
846
|
+
let res;
|
|
847
|
+
try {
|
|
848
|
+
res = matchResult[0][0][0][0](c, async () => {
|
|
849
|
+
c.res = await this.#notFoundHandler(c);
|
|
850
|
+
});
|
|
851
|
+
} catch (err) {
|
|
852
|
+
return this.#handleError(err, c);
|
|
853
|
+
}
|
|
854
|
+
return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c);
|
|
855
|
+
}
|
|
856
|
+
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
|
|
857
|
+
return (async () => {
|
|
858
|
+
try {
|
|
859
|
+
const context = await composed(c);
|
|
860
|
+
if (!context.finalized)
|
|
861
|
+
throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
|
|
862
|
+
return context.res;
|
|
863
|
+
} catch (err) {
|
|
864
|
+
return this.#handleError(err, c);
|
|
865
|
+
}
|
|
866
|
+
})();
|
|
867
|
+
}
|
|
868
|
+
fetch = (request, ...rest) => {
|
|
869
|
+
return this.#dispatch(request, rest[1], rest[0], request.method);
|
|
870
|
+
};
|
|
871
|
+
request = (input, requestInit, Env, executionCtx) => {
|
|
872
|
+
if (input instanceof Request)
|
|
873
|
+
return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
|
|
874
|
+
input = input.toString();
|
|
875
|
+
return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx);
|
|
876
|
+
};
|
|
877
|
+
fire = () => {
|
|
878
|
+
addEventListener("fetch", (event) => {
|
|
879
|
+
event.respondWith(this.#dispatch(event.request, event, undefined, event.request.method));
|
|
880
|
+
});
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// node_modules/hono/dist/router/utils.js
|
|
885
|
+
var createNullObject = () => Object.create(null);
|
|
886
|
+
|
|
887
|
+
// node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
888
|
+
var emptyParam = [];
|
|
889
|
+
function match(method, path) {
|
|
890
|
+
const matchers = this.buildAllMatchers();
|
|
891
|
+
const match2 = (method2, path2) => {
|
|
892
|
+
const matcher = matchers[method2] || matchers["ALL"];
|
|
893
|
+
const staticMatch = matcher[2][path2];
|
|
894
|
+
if (staticMatch)
|
|
895
|
+
return staticMatch;
|
|
896
|
+
const match3 = path2.match(matcher[0]);
|
|
897
|
+
if (!match3)
|
|
898
|
+
return [[], emptyParam];
|
|
899
|
+
const index = match3.indexOf("", 1);
|
|
900
|
+
return [matcher[1][index], match3];
|
|
901
|
+
};
|
|
902
|
+
this.match = match2;
|
|
903
|
+
return match2(method, path);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// node_modules/hono/dist/router/reg-exp-router/node.js
|
|
907
|
+
var LABEL_REG_EXP_STR = "[^/]+";
|
|
908
|
+
var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
|
|
909
|
+
var PATH_ERROR = Symbol();
|
|
910
|
+
var regExpMetaChars = /* @__PURE__ */ new Set(".\\+*[^]$()");
|
|
911
|
+
function compareKey(a, b) {
|
|
912
|
+
if (a.length === 1)
|
|
913
|
+
return b.length === 1 ? a < b ? -1 : 1 : -1;
|
|
914
|
+
if (b.length === 1)
|
|
915
|
+
return 1;
|
|
916
|
+
if (a === ".*" || a === "(?:|/.*)")
|
|
917
|
+
return b === "(?:|/.*)" ? -1 : 1;
|
|
918
|
+
else if (b === ".*" || b === "(?:|/.*)")
|
|
919
|
+
return -1;
|
|
920
|
+
if (a === "[^/]+")
|
|
921
|
+
return 1;
|
|
922
|
+
else if (b === "[^/]+")
|
|
923
|
+
return -1;
|
|
924
|
+
return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length;
|
|
925
|
+
}
|
|
926
|
+
var Node = class Node2 {
|
|
927
|
+
#index;
|
|
928
|
+
#varIndex;
|
|
929
|
+
#children = createNullObject();
|
|
930
|
+
insert(tokens, index, paramMap, context, isStatic) {
|
|
931
|
+
let node = this;
|
|
932
|
+
for (let i = 0, len = tokens.length;i < len; i++) {
|
|
933
|
+
const token = tokens[i];
|
|
934
|
+
const pattern = token.length === 1 ? token === "*" ? i === len - 1 ? [
|
|
935
|
+
"",
|
|
936
|
+
"",
|
|
937
|
+
".*"
|
|
938
|
+
] : [
|
|
939
|
+
"",
|
|
940
|
+
"",
|
|
941
|
+
LABEL_REG_EXP_STR
|
|
942
|
+
] : null : token === "/*" ? [
|
|
943
|
+
"",
|
|
944
|
+
"",
|
|
945
|
+
TAIL_WILDCARD_REG_EXP_STR
|
|
946
|
+
] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
947
|
+
let nextNode;
|
|
948
|
+
if (pattern) {
|
|
949
|
+
const name = pattern[1];
|
|
950
|
+
let regexpStr = pattern[2] || "[^/]+";
|
|
951
|
+
if (name && pattern[2]) {
|
|
952
|
+
if (regexpStr === ".*")
|
|
953
|
+
throw PATH_ERROR;
|
|
954
|
+
regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:");
|
|
955
|
+
if (/\((?!\?:)/.test(regexpStr))
|
|
956
|
+
throw PATH_ERROR;
|
|
957
|
+
if (regexpStr.length === 1 && regExpMetaChars.has(regexpStr))
|
|
958
|
+
throw PATH_ERROR;
|
|
959
|
+
}
|
|
960
|
+
nextNode = node.#children[regexpStr];
|
|
961
|
+
if (!nextNode) {
|
|
962
|
+
if (regexpStr !== ".*" && regexpStr !== "(?:|/.*)") {
|
|
963
|
+
for (const k in node.#children)
|
|
964
|
+
if ((regexpStr.length > 1 || k.length > 1) && k !== ".*" && k !== "(?:|/.*)")
|
|
965
|
+
throw PATH_ERROR;
|
|
966
|
+
}
|
|
967
|
+
nextNode = node.#children[regexpStr] = new Node2;
|
|
968
|
+
}
|
|
969
|
+
if (name !== "") {
|
|
970
|
+
nextNode.#varIndex ??= context.varIndex++;
|
|
971
|
+
paramMap.push([name, nextNode.#varIndex]);
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
nextNode = node.#children[token];
|
|
975
|
+
if (!nextNode) {
|
|
976
|
+
for (const k in node.#children)
|
|
977
|
+
if (k.length > 1 && k !== ".*" && k !== "(?:|/.*)")
|
|
978
|
+
throw PATH_ERROR;
|
|
979
|
+
nextNode = node.#children[token] = new Node2;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
node = nextNode;
|
|
983
|
+
}
|
|
984
|
+
if (node.#index !== undefined)
|
|
985
|
+
throw PATH_ERROR;
|
|
986
|
+
node.#index = isStatic ? -1 : index;
|
|
987
|
+
}
|
|
988
|
+
buildRegExpStr() {
|
|
989
|
+
const strList = Object.keys(this.#children).sort(compareKey).map((k) => {
|
|
990
|
+
const c = this.#children[k];
|
|
991
|
+
const childStr = c.buildRegExpStr();
|
|
992
|
+
return childStr === "" ? "" : (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + childStr;
|
|
993
|
+
}).filter(Boolean);
|
|
994
|
+
if (typeof this.#index === "number" && this.#index !== -1)
|
|
995
|
+
strList.unshift(`#${this.#index}`);
|
|
996
|
+
if (strList.length === 0)
|
|
997
|
+
return "";
|
|
998
|
+
if (strList.length === 1)
|
|
999
|
+
return strList[0];
|
|
1000
|
+
return "(?:" + strList.join("|") + ")";
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// node_modules/hono/dist/router/reg-exp-router/trie.js
|
|
1005
|
+
var Trie = class {
|
|
1006
|
+
#context = { varIndex: 0 };
|
|
1007
|
+
#root = new Node;
|
|
1008
|
+
#index = 0;
|
|
1009
|
+
paths = createNullObject();
|
|
1010
|
+
insert(path, isStatic) {
|
|
1011
|
+
if (isStatic) {
|
|
1012
|
+
this.#root.insert(path.split(""), 0, [], this.#context, true);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const paramAssoc = [];
|
|
1016
|
+
const groups = [];
|
|
1017
|
+
let markedPath = path;
|
|
1018
|
+
for (let i = 0;; ) {
|
|
1019
|
+
let replaced = false;
|
|
1020
|
+
markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
|
|
1021
|
+
const mark = `@\\${i}`;
|
|
1022
|
+
groups[i] = [mark, m];
|
|
1023
|
+
i++;
|
|
1024
|
+
replaced = true;
|
|
1025
|
+
return mark;
|
|
1026
|
+
});
|
|
1027
|
+
if (!replaced)
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
const tokens = markedPath.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
|
|
1031
|
+
for (let i = groups.length - 1;i >= 0; i--) {
|
|
1032
|
+
const [mark] = groups[i];
|
|
1033
|
+
for (let j = tokens.length - 1;j >= 0; j--)
|
|
1034
|
+
if (tokens[j].indexOf(mark) !== -1) {
|
|
1035
|
+
tokens[j] = tokens[j].replace(mark, groups[i][1]);
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
|
|
1040
|
+
this.paths[path] = [this.#index++, paramAssoc];
|
|
1041
|
+
}
|
|
1042
|
+
buildRegExp() {
|
|
1043
|
+
let regexp = this.#root.buildRegExpStr();
|
|
1044
|
+
if (regexp === "")
|
|
1045
|
+
return [
|
|
1046
|
+
/^$/,
|
|
1047
|
+
[],
|
|
1048
|
+
[]
|
|
1049
|
+
];
|
|
1050
|
+
let captureIndex = 0;
|
|
1051
|
+
const indexReplacementMap = [];
|
|
1052
|
+
const paramReplacementMap = [];
|
|
1053
|
+
regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => {
|
|
1054
|
+
if (handlerIndex !== undefined) {
|
|
1055
|
+
indexReplacementMap[++captureIndex] = Number(handlerIndex);
|
|
1056
|
+
return "$()";
|
|
1057
|
+
}
|
|
1058
|
+
if (paramIndex !== undefined) {
|
|
1059
|
+
paramReplacementMap[Number(paramIndex)] = ++captureIndex;
|
|
1060
|
+
return "";
|
|
1061
|
+
}
|
|
1062
|
+
return "";
|
|
1063
|
+
});
|
|
1064
|
+
return [
|
|
1065
|
+
new RegExp(`^${regexp}`),
|
|
1066
|
+
indexReplacementMap,
|
|
1067
|
+
paramReplacementMap
|
|
1068
|
+
];
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// node_modules/hono/dist/router/reg-exp-router/router.js
|
|
1073
|
+
var wildcardRegExpCache = createNullObject();
|
|
1074
|
+
function buildWildcardRegExp(path) {
|
|
1075
|
+
return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`);
|
|
1076
|
+
}
|
|
1077
|
+
function findMiddleware(middleware, path) {
|
|
1078
|
+
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length))
|
|
1079
|
+
if (buildWildcardRegExp(k).test(path))
|
|
1080
|
+
return [...middleware[k]];
|
|
1081
|
+
}
|
|
1082
|
+
var RegExpRouter = class {
|
|
1083
|
+
name = "RegExpRouter";
|
|
1084
|
+
#middleware;
|
|
1085
|
+
#routes;
|
|
1086
|
+
#tries;
|
|
1087
|
+
constructor() {
|
|
1088
|
+
this.#middleware = { ["ALL"]: createNullObject() };
|
|
1089
|
+
this.#routes = { ["ALL"]: createNullObject() };
|
|
1090
|
+
this.#tries = { ["ALL"]: new Trie };
|
|
1091
|
+
}
|
|
1092
|
+
#insertPath(method, path) {
|
|
1093
|
+
try {
|
|
1094
|
+
this.#tries[method].insert(path, !/\*|\/:/.test(path));
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
throw e === PATH_ERROR ? new UnsupportedPathError(path) : e;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
add(method, path, handler) {
|
|
1100
|
+
const middleware = this.#middleware;
|
|
1101
|
+
const routes = this.#routes;
|
|
1102
|
+
if (!middleware)
|
|
1103
|
+
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
1104
|
+
if (!middleware[method]) {
|
|
1105
|
+
this.#tries[method] = new Trie;
|
|
1106
|
+
for (const handlerMap of [middleware, routes]) {
|
|
1107
|
+
handlerMap[method] = createNullObject();
|
|
1108
|
+
for (const p in handlerMap["ALL"]) {
|
|
1109
|
+
handlerMap[method][p] = [...handlerMap["ALL"][p]];
|
|
1110
|
+
this.#insertPath(method, p);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (path === "/*")
|
|
1115
|
+
path = "*";
|
|
1116
|
+
const methods = method === "ALL" ? Object.keys(middleware) : [method];
|
|
1117
|
+
if (/\*$/.test(path)) {
|
|
1118
|
+
const re = buildWildcardRegExp(path);
|
|
1119
|
+
for (const m of methods)
|
|
1120
|
+
if (!middleware[m][path]) {
|
|
1121
|
+
this.#insertPath(m, path);
|
|
1122
|
+
middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || [];
|
|
1123
|
+
}
|
|
1124
|
+
for (const handlerMap of [middleware, routes])
|
|
1125
|
+
for (const m of methods)
|
|
1126
|
+
for (const p in handlerMap[m])
|
|
1127
|
+
re.test(p) && handlerMap[m][p].push([handler, path]);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const paths = checkOptionalParameter(path) || [path];
|
|
1131
|
+
for (const path2 of paths)
|
|
1132
|
+
for (const m of methods) {
|
|
1133
|
+
if (!routes[m][path2]) {
|
|
1134
|
+
this.#insertPath(m, path2);
|
|
1135
|
+
routes[m][path2] = findMiddleware(middleware[m], path2) || findMiddleware(middleware["ALL"], path2) || [];
|
|
1136
|
+
}
|
|
1137
|
+
routes[m][path2].push([handler, path2]);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
match = match;
|
|
1141
|
+
buildAllMatchers() {
|
|
1142
|
+
const matchers = createNullObject();
|
|
1143
|
+
for (const method of Object.keys(this.#routes))
|
|
1144
|
+
matchers[method] = this.#buildMatcher(method);
|
|
1145
|
+
this.#middleware = this.#routes = this.#tries = undefined;
|
|
1146
|
+
wildcardRegExpCache = createNullObject();
|
|
1147
|
+
return matchers;
|
|
1148
|
+
}
|
|
1149
|
+
#buildMatcher(method) {
|
|
1150
|
+
const middleware = this.#middleware[method];
|
|
1151
|
+
const routes = this.#routes[method];
|
|
1152
|
+
const trie = this.#tries[method];
|
|
1153
|
+
const staticMap = createNullObject();
|
|
1154
|
+
const handlerData = [];
|
|
1155
|
+
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
|
|
1156
|
+
for (const r of [middleware, routes])
|
|
1157
|
+
for (const path in r) {
|
|
1158
|
+
const handlers = r[path];
|
|
1159
|
+
const pathData = trie.paths[path];
|
|
1160
|
+
if (!pathData) {
|
|
1161
|
+
staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => {
|
|
1165
|
+
map[key] = paramReplacementMap[pathData[1][i][1]];
|
|
1166
|
+
return map;
|
|
1167
|
+
}, createNullObject())]);
|
|
1168
|
+
}
|
|
1169
|
+
return [
|
|
1170
|
+
regexp,
|
|
1171
|
+
indexReplacementMap.map((i) => handlerData[i]),
|
|
1172
|
+
staticMap
|
|
1173
|
+
];
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// node_modules/hono/dist/router/reg-exp-router/prepared-router.js
|
|
1178
|
+
var PreparedRegExpRouter = class {
|
|
1179
|
+
name = "PreparedRegExpRouter";
|
|
1180
|
+
#matchers;
|
|
1181
|
+
#relocateMap;
|
|
1182
|
+
constructor(matchers, relocateMap) {
|
|
1183
|
+
this.#matchers = matchers;
|
|
1184
|
+
this.#relocateMap = relocateMap;
|
|
1185
|
+
}
|
|
1186
|
+
#addWildcard(method, handlerData) {
|
|
1187
|
+
const matcher = this.#matchers[method];
|
|
1188
|
+
matcher[1].forEach((list) => list && list.push(handlerData));
|
|
1189
|
+
Object.values(matcher[2]).forEach((list) => list[0].push(handlerData));
|
|
1190
|
+
}
|
|
1191
|
+
#addPath(method, path, handler, indexes, map) {
|
|
1192
|
+
const matcher = this.#matchers[method];
|
|
1193
|
+
if (!map)
|
|
1194
|
+
matcher[2][path][0].push([handler, {}]);
|
|
1195
|
+
else
|
|
1196
|
+
indexes.forEach((index) => {
|
|
1197
|
+
if (typeof index === "number")
|
|
1198
|
+
matcher[1][index].push([handler, map]);
|
|
1199
|
+
else
|
|
1200
|
+
matcher[2][index || path][0].push([handler, map]);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
add(method, path, handler) {
|
|
1204
|
+
if (!this.#matchers[method]) {
|
|
1205
|
+
const all = this.#matchers["ALL"];
|
|
1206
|
+
const staticMap = {};
|
|
1207
|
+
for (const key in all[2])
|
|
1208
|
+
staticMap[key] = [all[2][key][0].slice(), emptyParam];
|
|
1209
|
+
this.#matchers[method] = [
|
|
1210
|
+
all[0],
|
|
1211
|
+
all[1].map((list) => Array.isArray(list) ? list.slice() : 0),
|
|
1212
|
+
staticMap
|
|
1213
|
+
];
|
|
1214
|
+
}
|
|
1215
|
+
if (path === "/*" || path === "*") {
|
|
1216
|
+
const handlerData = [handler, {}];
|
|
1217
|
+
if (method === "ALL")
|
|
1218
|
+
for (const m in this.#matchers)
|
|
1219
|
+
this.#addWildcard(m, handlerData);
|
|
1220
|
+
else
|
|
1221
|
+
this.#addWildcard(method, handlerData);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const data = this.#relocateMap[path];
|
|
1225
|
+
if (!data)
|
|
1226
|
+
throw new Error(`Path ${path} is not registered`);
|
|
1227
|
+
for (const [indexes, map] of data)
|
|
1228
|
+
if (method === "ALL")
|
|
1229
|
+
for (const m in this.#matchers)
|
|
1230
|
+
this.#addPath(m, path, handler, indexes, map);
|
|
1231
|
+
else
|
|
1232
|
+
this.#addPath(method, path, handler, indexes, map);
|
|
1233
|
+
}
|
|
1234
|
+
buildAllMatchers() {
|
|
1235
|
+
return this.#matchers;
|
|
1236
|
+
}
|
|
1237
|
+
match = match;
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
// node_modules/hono/dist/router/smart-router/router.js
|
|
1241
|
+
var SmartRouter = class {
|
|
1242
|
+
name = "SmartRouter";
|
|
1243
|
+
#routers = [];
|
|
1244
|
+
#routes = [];
|
|
1245
|
+
constructor(init) {
|
|
1246
|
+
this.#routers = init.routers;
|
|
1247
|
+
}
|
|
1248
|
+
add(method, path, handler) {
|
|
1249
|
+
if (!this.#routes)
|
|
1250
|
+
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
1251
|
+
this.#routes.push([
|
|
1252
|
+
method,
|
|
1253
|
+
path,
|
|
1254
|
+
handler
|
|
1255
|
+
]);
|
|
1256
|
+
}
|
|
1257
|
+
match(method, path) {
|
|
1258
|
+
if (!this.#routes)
|
|
1259
|
+
throw new Error("Fatal error");
|
|
1260
|
+
const routers = this.#routers;
|
|
1261
|
+
const routes = this.#routes;
|
|
1262
|
+
const len = routers.length;
|
|
1263
|
+
let i = 0;
|
|
1264
|
+
let res;
|
|
1265
|
+
for (;i < len; i++) {
|
|
1266
|
+
const router = routers[i];
|
|
1267
|
+
try {
|
|
1268
|
+
for (let i2 = 0, len2 = routes.length;i2 < len2; i2++)
|
|
1269
|
+
router.add(...routes[i2]);
|
|
1270
|
+
res = router.match(method, path);
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
if (e instanceof UnsupportedPathError)
|
|
1273
|
+
continue;
|
|
1274
|
+
throw e;
|
|
1275
|
+
}
|
|
1276
|
+
this.match = router.match.bind(router);
|
|
1277
|
+
this.#routers = [router];
|
|
1278
|
+
this.#routes = undefined;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
if (i === len)
|
|
1282
|
+
throw new Error("Fatal error");
|
|
1283
|
+
this.name = `SmartRouter + ${this.activeRouter.name}`;
|
|
1284
|
+
return res;
|
|
1285
|
+
}
|
|
1286
|
+
get activeRouter() {
|
|
1287
|
+
if (this.#routes || this.#routers.length !== 1)
|
|
1288
|
+
throw new Error("No active router has been determined yet.");
|
|
1289
|
+
return this.#routers[0];
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// node_modules/hono/dist/router/trie-router/node.js
|
|
1294
|
+
var emptyParams = createNullObject();
|
|
1295
|
+
var order = 0;
|
|
1296
|
+
var Node3 = class Node4 {
|
|
1297
|
+
#methods = [];
|
|
1298
|
+
#children = createNullObject();
|
|
1299
|
+
#patterns = [];
|
|
1300
|
+
#pattern;
|
|
1301
|
+
#params = emptyParams;
|
|
1302
|
+
insert(method, path, handler) {
|
|
1303
|
+
let curNode = this;
|
|
1304
|
+
const parts = splitRoutingPath(path);
|
|
1305
|
+
const possibleKeys = /* @__PURE__ */ new Set;
|
|
1306
|
+
let i = 0;
|
|
1307
|
+
for (const p of parts) {
|
|
1308
|
+
const nextP = parts[++i];
|
|
1309
|
+
const pattern = getPattern(p, nextP) || (nextP === undefined && p && p.indexOf("*") === p.length - 1 ? p : null);
|
|
1310
|
+
const isParam = Array.isArray(pattern);
|
|
1311
|
+
const key = isParam ? pattern[0] : pattern || p;
|
|
1312
|
+
const child = curNode.#children[key] ||= new Node4;
|
|
1313
|
+
if (pattern && !child.#pattern) {
|
|
1314
|
+
child.#pattern = pattern;
|
|
1315
|
+
curNode.#patterns.push(child);
|
|
1316
|
+
}
|
|
1317
|
+
curNode = child;
|
|
1318
|
+
if (isParam)
|
|
1319
|
+
possibleKeys.add(pattern[1]);
|
|
1320
|
+
}
|
|
1321
|
+
curNode.#methods.push({ [method]: {
|
|
1322
|
+
handler,
|
|
1323
|
+
possibleKeys: [...possibleKeys],
|
|
1324
|
+
score: ++order
|
|
1325
|
+
} });
|
|
1326
|
+
}
|
|
1327
|
+
#pushHandlerSets(handlerSets, node, method, nodeParams, params) {
|
|
1328
|
+
for (let i = 0, len = node.#methods.length;i < len; i++) {
|
|
1329
|
+
const m = node.#methods[i];
|
|
1330
|
+
const handlerSet = m[method] || m["ALL"];
|
|
1331
|
+
if (handlerSet) {
|
|
1332
|
+
handlerSet.params = createNullObject();
|
|
1333
|
+
handlerSets.push(handlerSet);
|
|
1334
|
+
for (let i2 = 0, len2 = handlerSet.possibleKeys.length;i2 < len2; i2++) {
|
|
1335
|
+
const key = handlerSet.possibleKeys[i2];
|
|
1336
|
+
handlerSet.params[key] = params?.[key] && !i2 ? params[key] : nodeParams[key] ?? params?.[key];
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
search(method, path) {
|
|
1342
|
+
const handlerSets = [];
|
|
1343
|
+
this.#params = emptyParams;
|
|
1344
|
+
let curNodes = [this];
|
|
1345
|
+
const parts = splitPath(path);
|
|
1346
|
+
const curNodesQueue = [];
|
|
1347
|
+
const len = parts.length;
|
|
1348
|
+
let partOffsets = null;
|
|
1349
|
+
for (let i = 0;i < len; i++) {
|
|
1350
|
+
const part = parts[i];
|
|
1351
|
+
const isLast = i === len - 1;
|
|
1352
|
+
const tempNodes = [];
|
|
1353
|
+
for (let j = 0, len2 = curNodes.length;j < len2; j++) {
|
|
1354
|
+
const node = curNodes[j];
|
|
1355
|
+
const nextNode = node.#children[part];
|
|
1356
|
+
if (nextNode) {
|
|
1357
|
+
nextNode.#params = node.#params;
|
|
1358
|
+
if (isLast) {
|
|
1359
|
+
if (nextNode.#children["*"])
|
|
1360
|
+
this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params);
|
|
1361
|
+
this.#pushHandlerSets(handlerSets, nextNode, method, node.#params);
|
|
1362
|
+
} else
|
|
1363
|
+
tempNodes.push(nextNode);
|
|
1364
|
+
}
|
|
1365
|
+
for (const child of node.#patterns) {
|
|
1366
|
+
const pattern = child.#pattern;
|
|
1367
|
+
const params = node.#params === emptyParams ? {} : { ...node.#params };
|
|
1368
|
+
if (typeof pattern === "string") {
|
|
1369
|
+
if (pattern === "*" || part.startsWith(pattern.slice(0, -1))) {
|
|
1370
|
+
this.#pushHandlerSets(handlerSets, child, method, node.#params);
|
|
1371
|
+
if (pattern === "*") {
|
|
1372
|
+
child.#params = params;
|
|
1373
|
+
tempNodes.push(child);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const [, name, matcher] = pattern;
|
|
1379
|
+
if (!part && matcher === true)
|
|
1380
|
+
continue;
|
|
1381
|
+
if (matcher !== true) {
|
|
1382
|
+
if (!partOffsets) {
|
|
1383
|
+
partOffsets = [];
|
|
1384
|
+
let offset = path[0] === "/" ? 1 : 0;
|
|
1385
|
+
for (let p = 0;p < len; p++) {
|
|
1386
|
+
partOffsets[p] = offset;
|
|
1387
|
+
offset += parts[p].length + 1;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const restPathString = path.slice(partOffsets[i]);
|
|
1391
|
+
const m = matcher.exec(restPathString);
|
|
1392
|
+
if (m) {
|
|
1393
|
+
params[name] = m[0];
|
|
1394
|
+
this.#pushHandlerSets(handlerSets, child, method, node.#params, params);
|
|
1395
|
+
if (m[0].length === restPathString.length && child.#children["*"])
|
|
1396
|
+
this.#pushHandlerSets(handlerSets, child.#children["*"], method, node.#params, params);
|
|
1397
|
+
for (const _ in child.#children) {
|
|
1398
|
+
child.#params = params;
|
|
1399
|
+
const componentCount = m[0].match(/\//g)?.length ?? 0;
|
|
1400
|
+
(curNodesQueue[componentCount] ||= []).push(child);
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (matcher === true || matcher.test(part)) {
|
|
1407
|
+
params[name] = part;
|
|
1408
|
+
if (isLast) {
|
|
1409
|
+
this.#pushHandlerSets(handlerSets, child, method, params, node.#params);
|
|
1410
|
+
if (child.#children["*"])
|
|
1411
|
+
this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params);
|
|
1412
|
+
} else {
|
|
1413
|
+
child.#params = params;
|
|
1414
|
+
tempNodes.push(child);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const shifted = curNodesQueue.shift();
|
|
1420
|
+
curNodes = shifted ? tempNodes.concat(shifted) : tempNodes;
|
|
1421
|
+
}
|
|
1422
|
+
if (handlerSets[1])
|
|
1423
|
+
handlerSets.sort((a, b) => {
|
|
1424
|
+
return a.score - b.score;
|
|
1425
|
+
});
|
|
1426
|
+
return [handlerSets.map(({ handler, params }) => [handler, params])];
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// node_modules/hono/dist/router/trie-router/router.js
|
|
1431
|
+
var TrieRouter = class {
|
|
1432
|
+
name = "TrieRouter";
|
|
1433
|
+
#node = new Node3;
|
|
1434
|
+
add(method, path, handler) {
|
|
1435
|
+
for (const result of checkOptionalParameter(path) || [path])
|
|
1436
|
+
this.#node.insert(method, result, handler);
|
|
1437
|
+
}
|
|
1438
|
+
match(method, path) {
|
|
1439
|
+
return this.#node.search(method, path);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// node_modules/hono/dist/hono.js
|
|
1444
|
+
var Hono3 = class extends Hono {
|
|
1445
|
+
constructor(options = {}) {
|
|
1446
|
+
super(options);
|
|
1447
|
+
this.router = options.router ?? new SmartRouter({ routers: [new RegExpRouter, new TrieRouter] });
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// src/core/inline-text.ts
|
|
1452
|
+
function normalizeMixedTextSpacing(text) {
|
|
1453
|
+
return text.replace(/[ \t]+/g, " ");
|
|
1454
|
+
}
|
|
1455
|
+
function parseInlineRuns(text, highlightWords = []) {
|
|
1456
|
+
const runs = [];
|
|
1457
|
+
const escapedWords = highlightWords.filter((word) => word.length > 0).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1458
|
+
const hlRegex = escapedWords.length ? new RegExp(`(${escapedWords.join("|")})`, "g") : null;
|
|
1459
|
+
const pattern = /(\*\*[^*]+\*\*|\*[^*]+\*|==[^=]+==)/g;
|
|
1460
|
+
let lastIndex = 0;
|
|
1461
|
+
let match2;
|
|
1462
|
+
function pushTextWithHighlights(str, baseStyle = {}) {
|
|
1463
|
+
if (!str)
|
|
1464
|
+
return;
|
|
1465
|
+
if (!hlRegex) {
|
|
1466
|
+
runs.push({ text: str, ...baseStyle });
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
hlRegex.lastIndex = 0;
|
|
1470
|
+
let hlLast = 0;
|
|
1471
|
+
let hlMatch;
|
|
1472
|
+
while ((hlMatch = hlRegex.exec(str)) !== null) {
|
|
1473
|
+
if (hlMatch.index > hlLast) {
|
|
1474
|
+
runs.push({ text: str.slice(hlLast, hlMatch.index), ...baseStyle });
|
|
1475
|
+
}
|
|
1476
|
+
runs.push({ text: hlMatch[0], ...baseStyle, highlight: true });
|
|
1477
|
+
hlLast = hlMatch.index + hlMatch[0].length;
|
|
1478
|
+
}
|
|
1479
|
+
if (hlLast < str.length) {
|
|
1480
|
+
runs.push({ text: str.slice(hlLast), ...baseStyle });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
while ((match2 = pattern.exec(text)) !== null) {
|
|
1484
|
+
if (match2.index > lastIndex) {
|
|
1485
|
+
pushTextWithHighlights(text.slice(lastIndex, match2.index));
|
|
1486
|
+
}
|
|
1487
|
+
const token = match2[0];
|
|
1488
|
+
if (token.startsWith("**") && token.endsWith("**")) {
|
|
1489
|
+
pushTextWithHighlights(token.slice(2, -2), { bold: true });
|
|
1490
|
+
} else if (token.startsWith("*") && token.endsWith("*")) {
|
|
1491
|
+
pushTextWithHighlights(token.slice(1, -1), { italic: true });
|
|
1492
|
+
} else if (token.startsWith("==") && token.endsWith("==")) {
|
|
1493
|
+
runs.push({ text: token.slice(2, -2), highlight: true, bold: true });
|
|
1494
|
+
}
|
|
1495
|
+
lastIndex = match2.index + token.length;
|
|
1496
|
+
}
|
|
1497
|
+
if (lastIndex < text.length) {
|
|
1498
|
+
pushTextWithHighlights(text.slice(lastIndex));
|
|
1499
|
+
}
|
|
1500
|
+
return runs.length > 0 ? runs : [{ text }];
|
|
1501
|
+
}
|
|
1502
|
+
function parseContentBlocks(rawMarkdown) {
|
|
1503
|
+
const lines = rawMarkdown.replaceAll(`\r
|
|
1504
|
+
`, `
|
|
1505
|
+
`).split(`
|
|
1506
|
+
`);
|
|
1507
|
+
const blocks = [];
|
|
1508
|
+
let paragraphBuffer = [];
|
|
1509
|
+
function flushParagraph() {
|
|
1510
|
+
const value = normalizeMixedTextSpacing(paragraphBuffer.join(" ").trim());
|
|
1511
|
+
if (value.length > 0) {
|
|
1512
|
+
blocks.push({ kind: "paragraph", text: value });
|
|
1513
|
+
}
|
|
1514
|
+
paragraphBuffer = [];
|
|
1515
|
+
}
|
|
1516
|
+
for (const rawLine of lines) {
|
|
1517
|
+
const line = rawLine.trim();
|
|
1518
|
+
if (line.length === 0) {
|
|
1519
|
+
flushParagraph();
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (isPageMarkerLine(line)) {
|
|
1523
|
+
flushParagraph();
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1527
|
+
if (heading) {
|
|
1528
|
+
flushParagraph();
|
|
1529
|
+
const level = heading[1].length;
|
|
1530
|
+
blocks.push({
|
|
1531
|
+
kind: level === 1 ? "heading" : level === 2 ? "subheading" : "minor-heading",
|
|
1532
|
+
text: normalizeMixedTextSpacing(heading[2])
|
|
1533
|
+
});
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (line.startsWith(">")) {
|
|
1537
|
+
flushParagraph();
|
|
1538
|
+
blocks.push({
|
|
1539
|
+
kind: "quote",
|
|
1540
|
+
text: normalizeMixedTextSpacing(line.replace(/^>\s?/, "").trim())
|
|
1541
|
+
});
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
const listItem = line.match(/^(\d+[.)]|[-*+])\s+(.+)$/);
|
|
1545
|
+
if (listItem) {
|
|
1546
|
+
flushParagraph();
|
|
1547
|
+
blocks.push({
|
|
1548
|
+
kind: "list-item",
|
|
1549
|
+
text: normalizeMixedTextSpacing(listItem[2]),
|
|
1550
|
+
bullet: /^\d/.test(listItem[1]) ? listItem[1] : "•"
|
|
1551
|
+
});
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
paragraphBuffer.push(line);
|
|
1555
|
+
}
|
|
1556
|
+
flushParagraph();
|
|
1557
|
+
return blocks;
|
|
1558
|
+
}
|
|
1559
|
+
function isPageMarkerLine(line) {
|
|
1560
|
+
const trimmed = line.trim();
|
|
1561
|
+
return /^【第([一二三四五六七八九十百两\d]+)页】$/.test(trimmed) || /^【Page\s*(\d+)】$/i.test(trimmed) || /^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed) || /^<!--\s*(?:pagebreak|page-break|next-page|page(?::\s*\d+)?)\s*-->$/i.test(trimmed);
|
|
1562
|
+
}
|
|
1563
|
+
function splitTextByPages(text) {
|
|
1564
|
+
const lines = text.replaceAll(`\r
|
|
1565
|
+
`, `
|
|
1566
|
+
`).split(`
|
|
1567
|
+
`);
|
|
1568
|
+
const pages = [[]];
|
|
1569
|
+
let hasMarker = false;
|
|
1570
|
+
for (const line of lines) {
|
|
1571
|
+
if (isPageMarkerLine(line)) {
|
|
1572
|
+
hasMarker = true;
|
|
1573
|
+
if (pages.length === 1 && pages[0].length === 0) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
pages.push([]);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
pages[pages.length - 1].push(line);
|
|
1580
|
+
}
|
|
1581
|
+
if (!hasMarker) {
|
|
1582
|
+
return [text];
|
|
1583
|
+
}
|
|
1584
|
+
return pages.map((pageLines) => pageLines.join(`
|
|
1585
|
+
`).trim());
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// node_modules/@chenglou/pretext/dist/rich-inline.js
|
|
1589
|
+
var COLLAPSIBLE_BOUNDARY_RE = /[ \t\n\f\r]+/;
|
|
1590
|
+
var LEADING_COLLAPSIBLE_BOUNDARY_RE = /^[ \t\n\f\r]+/;
|
|
1591
|
+
var TRAILING_COLLAPSIBLE_BOUNDARY_RE = /[ \t\n\f\r]+$/;
|
|
1592
|
+
var EMPTY_LAYOUT_CURSOR = { segmentIndex: 0, graphemeIndex: 0 };
|
|
1593
|
+
var RICH_INLINE_START_CURSOR = {
|
|
1594
|
+
itemIndex: 0,
|
|
1595
|
+
segmentIndex: 0,
|
|
1596
|
+
graphemeIndex: 0
|
|
1597
|
+
};
|
|
1598
|
+
function getInternalPreparedRichInline(prepared) {
|
|
1599
|
+
return prepared;
|
|
1600
|
+
}
|
|
1601
|
+
function cloneCursor(cursor) {
|
|
1602
|
+
return {
|
|
1603
|
+
segmentIndex: cursor.segmentIndex,
|
|
1604
|
+
graphemeIndex: cursor.graphemeIndex
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function isLineStartCursor(cursor) {
|
|
1608
|
+
return cursor.segmentIndex === 0 && cursor.graphemeIndex === 0;
|
|
1609
|
+
}
|
|
1610
|
+
function getCollapsedSpaceWidth(font, letterSpacing, cache) {
|
|
1611
|
+
const cacheKey = `${font}\x00${letterSpacing}`;
|
|
1612
|
+
const cached = cache.get(cacheKey);
|
|
1613
|
+
if (cached !== undefined)
|
|
1614
|
+
return cached;
|
|
1615
|
+
const options = letterSpacing === 0 ? undefined : { letterSpacing };
|
|
1616
|
+
const joinedWidth = measureNaturalWidth(prepareWithSegments("A A", font, options));
|
|
1617
|
+
const compactWidth = measureNaturalWidth(prepareWithSegments("AA", font, options));
|
|
1618
|
+
const collapsedWidth = Math.max(0, joinedWidth - compactWidth);
|
|
1619
|
+
cache.set(cacheKey, collapsedWidth);
|
|
1620
|
+
return collapsedWidth;
|
|
1621
|
+
}
|
|
1622
|
+
function prepareWholeItemLine(prepared) {
|
|
1623
|
+
const end = { segmentIndex: 0, graphemeIndex: 0 };
|
|
1624
|
+
const width = stepPreparedLineGeometry(prepared, end, Number.POSITIVE_INFINITY);
|
|
1625
|
+
if (width === null)
|
|
1626
|
+
return null;
|
|
1627
|
+
return {
|
|
1628
|
+
endGraphemeIndex: end.graphemeIndex,
|
|
1629
|
+
endSegmentIndex: end.segmentIndex,
|
|
1630
|
+
width
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function endsInsideFirstSegment(segmentIndex, graphemeIndex) {
|
|
1634
|
+
return segmentIndex === 0 && graphemeIndex > 0;
|
|
1635
|
+
}
|
|
1636
|
+
function prepareRichInline(items) {
|
|
1637
|
+
const preparedItems = [];
|
|
1638
|
+
const itemsBySourceItemIndex = Array.from({ length: items.length });
|
|
1639
|
+
const collapsedSpaceWidthCache = new Map;
|
|
1640
|
+
let pendingGapWidth = 0;
|
|
1641
|
+
for (let index = 0;index < items.length; index++) {
|
|
1642
|
+
const item = items[index];
|
|
1643
|
+
const letterSpacing = item.letterSpacing ?? 0;
|
|
1644
|
+
const hasLeadingWhitespace = LEADING_COLLAPSIBLE_BOUNDARY_RE.test(item.text);
|
|
1645
|
+
const hasTrailingWhitespace = TRAILING_COLLAPSIBLE_BOUNDARY_RE.test(item.text);
|
|
1646
|
+
const trimmedText = item.text.replace(LEADING_COLLAPSIBLE_BOUNDARY_RE, "").replace(TRAILING_COLLAPSIBLE_BOUNDARY_RE, "");
|
|
1647
|
+
if (trimmedText.length === 0) {
|
|
1648
|
+
if (COLLAPSIBLE_BOUNDARY_RE.test(item.text) && pendingGapWidth === 0) {
|
|
1649
|
+
pendingGapWidth = getCollapsedSpaceWidth(item.font, letterSpacing, collapsedSpaceWidthCache);
|
|
1650
|
+
}
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const gapBefore = pendingGapWidth > 0 ? pendingGapWidth : hasLeadingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing, collapsedSpaceWidthCache) : 0;
|
|
1654
|
+
const prepared = prepareWithSegments(trimmedText, item.font, letterSpacing === 0 ? undefined : { letterSpacing });
|
|
1655
|
+
const wholeLine = prepareWholeItemLine(prepared);
|
|
1656
|
+
if (wholeLine === null) {
|
|
1657
|
+
pendingGapWidth = hasTrailingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing, collapsedSpaceWidthCache) : 0;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
const preparedItem = {
|
|
1661
|
+
break: item.break ?? "normal",
|
|
1662
|
+
endGraphemeIndex: wholeLine.endGraphemeIndex,
|
|
1663
|
+
endSegmentIndex: wholeLine.endSegmentIndex,
|
|
1664
|
+
extraWidth: item.extraWidth ?? 0,
|
|
1665
|
+
gapBefore,
|
|
1666
|
+
naturalWidth: wholeLine.width,
|
|
1667
|
+
prepared,
|
|
1668
|
+
sourceItemIndex: index
|
|
1669
|
+
};
|
|
1670
|
+
preparedItems.push(preparedItem);
|
|
1671
|
+
itemsBySourceItemIndex[index] = preparedItem;
|
|
1672
|
+
pendingGapWidth = hasTrailingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing, collapsedSpaceWidthCache) : 0;
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
items: preparedItems,
|
|
1676
|
+
itemsBySourceItemIndex
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function stepRichInlineLine(flow, maxWidth, cursor, collectFragment) {
|
|
1680
|
+
if (flow.items.length === 0 || cursor.itemIndex >= flow.items.length)
|
|
1681
|
+
return null;
|
|
1682
|
+
const safeWidth = Math.max(1, maxWidth);
|
|
1683
|
+
let lineWidth = 0;
|
|
1684
|
+
let remainingWidth = safeWidth;
|
|
1685
|
+
let itemIndex = cursor.itemIndex;
|
|
1686
|
+
lineLoop:
|
|
1687
|
+
while (itemIndex < flow.items.length) {
|
|
1688
|
+
const item = flow.items[itemIndex];
|
|
1689
|
+
if (!isLineStartCursor(cursor) && cursor.segmentIndex === item.endSegmentIndex && cursor.graphemeIndex === item.endGraphemeIndex) {
|
|
1690
|
+
itemIndex++;
|
|
1691
|
+
cursor.segmentIndex = 0;
|
|
1692
|
+
cursor.graphemeIndex = 0;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
const gapBefore = lineWidth === 0 ? 0 : item.gapBefore;
|
|
1696
|
+
const atItemStart = isLineStartCursor(cursor);
|
|
1697
|
+
if (item.break === "never") {
|
|
1698
|
+
if (!atItemStart) {
|
|
1699
|
+
itemIndex++;
|
|
1700
|
+
cursor.segmentIndex = 0;
|
|
1701
|
+
cursor.graphemeIndex = 0;
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
const occupiedWidth = item.naturalWidth + item.extraWidth;
|
|
1705
|
+
const totalWidth = gapBefore + occupiedWidth;
|
|
1706
|
+
if (lineWidth > 0 && totalWidth > remainingWidth)
|
|
1707
|
+
break lineLoop;
|
|
1708
|
+
collectFragment?.(item, gapBefore, occupiedWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
|
|
1709
|
+
segmentIndex: item.endSegmentIndex,
|
|
1710
|
+
graphemeIndex: item.endGraphemeIndex
|
|
1711
|
+
});
|
|
1712
|
+
lineWidth += totalWidth;
|
|
1713
|
+
remainingWidth = Math.max(0, safeWidth - lineWidth);
|
|
1714
|
+
itemIndex++;
|
|
1715
|
+
cursor.segmentIndex = 0;
|
|
1716
|
+
cursor.graphemeIndex = 0;
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const reservedWidth = gapBefore + item.extraWidth;
|
|
1720
|
+
if (lineWidth > 0 && reservedWidth >= remainingWidth)
|
|
1721
|
+
break lineLoop;
|
|
1722
|
+
if (atItemStart) {
|
|
1723
|
+
const totalWidth = reservedWidth + item.naturalWidth;
|
|
1724
|
+
if (totalWidth <= remainingWidth) {
|
|
1725
|
+
collectFragment?.(item, gapBefore, item.naturalWidth + item.extraWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
|
|
1726
|
+
segmentIndex: item.endSegmentIndex,
|
|
1727
|
+
graphemeIndex: item.endGraphemeIndex
|
|
1728
|
+
});
|
|
1729
|
+
lineWidth += totalWidth;
|
|
1730
|
+
remainingWidth = Math.max(0, safeWidth - lineWidth);
|
|
1731
|
+
itemIndex++;
|
|
1732
|
+
cursor.segmentIndex = 0;
|
|
1733
|
+
cursor.graphemeIndex = 0;
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const availableWidth = Math.max(1, remainingWidth - reservedWidth);
|
|
1738
|
+
const lineEnd = {
|
|
1739
|
+
segmentIndex: cursor.segmentIndex,
|
|
1740
|
+
graphemeIndex: cursor.graphemeIndex
|
|
1741
|
+
};
|
|
1742
|
+
const lineWidthForItem = stepPreparedLineGeometry(item.prepared, lineEnd, availableWidth);
|
|
1743
|
+
if (lineWidthForItem === null) {
|
|
1744
|
+
itemIndex++;
|
|
1745
|
+
cursor.segmentIndex = 0;
|
|
1746
|
+
cursor.graphemeIndex = 0;
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (cursor.segmentIndex === lineEnd.segmentIndex && cursor.graphemeIndex === lineEnd.graphemeIndex) {
|
|
1750
|
+
itemIndex++;
|
|
1751
|
+
cursor.segmentIndex = 0;
|
|
1752
|
+
cursor.graphemeIndex = 0;
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
const itemOccupiedWidth = lineWidthForItem + item.extraWidth;
|
|
1756
|
+
const lineWidthContribution = gapBefore + itemOccupiedWidth;
|
|
1757
|
+
if (lineWidth > 0 && atItemStart && lineWidthContribution > remainingWidth)
|
|
1758
|
+
break lineLoop;
|
|
1759
|
+
if (lineWidth > 0 && atItemStart && gapBefore > 0 && endsInsideFirstSegment(lineEnd.segmentIndex, lineEnd.graphemeIndex)) {
|
|
1760
|
+
const freshLineEnd = { segmentIndex: 0, graphemeIndex: 0 };
|
|
1761
|
+
const freshLineWidth = stepPreparedLineGeometry(item.prepared, freshLineEnd, Math.max(1, safeWidth - item.extraWidth));
|
|
1762
|
+
if (freshLineWidth !== null && (freshLineEnd.segmentIndex > lineEnd.segmentIndex || freshLineEnd.segmentIndex === lineEnd.segmentIndex && freshLineEnd.graphemeIndex > lineEnd.graphemeIndex)) {
|
|
1763
|
+
break lineLoop;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
collectFragment?.(item, gapBefore, itemOccupiedWidth, cloneCursor(cursor), {
|
|
1767
|
+
segmentIndex: lineEnd.segmentIndex,
|
|
1768
|
+
graphemeIndex: lineEnd.graphemeIndex
|
|
1769
|
+
});
|
|
1770
|
+
lineWidth += lineWidthContribution;
|
|
1771
|
+
remainingWidth = Math.max(0, safeWidth - lineWidth);
|
|
1772
|
+
if (lineEnd.segmentIndex === item.endSegmentIndex && lineEnd.graphemeIndex === item.endGraphemeIndex) {
|
|
1773
|
+
itemIndex++;
|
|
1774
|
+
cursor.segmentIndex = 0;
|
|
1775
|
+
cursor.graphemeIndex = 0;
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
cursor.segmentIndex = lineEnd.segmentIndex;
|
|
1779
|
+
cursor.graphemeIndex = lineEnd.graphemeIndex;
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
if (lineWidth === 0)
|
|
1783
|
+
return null;
|
|
1784
|
+
cursor.itemIndex = itemIndex;
|
|
1785
|
+
return lineWidth;
|
|
1786
|
+
}
|
|
1787
|
+
function layoutNextRichInlineLineRange(prepared, maxWidth, start = RICH_INLINE_START_CURSOR) {
|
|
1788
|
+
const flow = getInternalPreparedRichInline(prepared);
|
|
1789
|
+
const end = {
|
|
1790
|
+
itemIndex: start.itemIndex,
|
|
1791
|
+
segmentIndex: start.segmentIndex,
|
|
1792
|
+
graphemeIndex: start.graphemeIndex
|
|
1793
|
+
};
|
|
1794
|
+
const fragments = [];
|
|
1795
|
+
const width = stepRichInlineLine(flow, maxWidth, end, (item, gapBefore, occupiedWidth, fragmentStart, fragmentEnd) => {
|
|
1796
|
+
fragments.push({
|
|
1797
|
+
itemIndex: item.sourceItemIndex,
|
|
1798
|
+
gapBefore,
|
|
1799
|
+
occupiedWidth,
|
|
1800
|
+
start: fragmentStart,
|
|
1801
|
+
end: fragmentEnd
|
|
1802
|
+
});
|
|
1803
|
+
});
|
|
1804
|
+
if (width === null)
|
|
1805
|
+
return null;
|
|
1806
|
+
return {
|
|
1807
|
+
fragments,
|
|
1808
|
+
width,
|
|
1809
|
+
end
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
function materializeFragmentText(item, fragment) {
|
|
1813
|
+
return buildLineTextFromRange(item.prepared, getLineTextCache(item.prepared), fragment.start.segmentIndex, fragment.start.graphemeIndex, fragment.end.segmentIndex, fragment.end.graphemeIndex);
|
|
1814
|
+
}
|
|
1815
|
+
function materializeRichInlineLineRange(prepared, line) {
|
|
1816
|
+
const flow = getInternalPreparedRichInline(prepared);
|
|
1817
|
+
const fragments = [];
|
|
1818
|
+
for (let i = 0;i < line.fragments.length; i++) {
|
|
1819
|
+
const fragment = line.fragments[i];
|
|
1820
|
+
const item = flow.itemsBySourceItemIndex[fragment.itemIndex];
|
|
1821
|
+
if (item === undefined)
|
|
1822
|
+
throw new Error("Missing rich-text inline item for fragment");
|
|
1823
|
+
fragments.push({
|
|
1824
|
+
itemIndex: fragment.itemIndex,
|
|
1825
|
+
text: materializeFragmentText(item, fragment),
|
|
1826
|
+
gapBefore: fragment.gapBefore,
|
|
1827
|
+
occupiedWidth: fragment.occupiedWidth,
|
|
1828
|
+
start: fragment.start,
|
|
1829
|
+
end: fragment.end
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
return {
|
|
1833
|
+
fragments,
|
|
1834
|
+
width: line.width,
|
|
1835
|
+
end: line.end
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/core/pretext.ts
|
|
1840
|
+
var requireFn = typeof process !== "undefined" && process.versions?.node ? (await import("node:module")).createRequire(import.meta.url) : undefined;
|
|
1841
|
+
var initialized = false;
|
|
1842
|
+
function ensurePretextRuntime() {
|
|
1843
|
+
if (initialized)
|
|
1844
|
+
return;
|
|
1845
|
+
if (typeof globalThis.OffscreenCanvas !== "undefined") {
|
|
1846
|
+
initialized = true;
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (typeof document !== "undefined") {
|
|
1850
|
+
const hidden = document.createElement("canvas");
|
|
1851
|
+
|
|
1852
|
+
class DomOffscreenCanvas {
|
|
1853
|
+
width = 1000;
|
|
1854
|
+
height = 1000;
|
|
1855
|
+
getContext(type) {
|
|
1856
|
+
return hidden.getContext(type);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
globalThis.OffscreenCanvas = DomOffscreenCanvas;
|
|
1860
|
+
initialized = true;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
try {
|
|
1864
|
+
if (requireFn) {
|
|
1865
|
+
const { createCanvas } = requireFn("@napi-rs/canvas");
|
|
1866
|
+
|
|
1867
|
+
class BunOffscreenCanvas {
|
|
1868
|
+
width;
|
|
1869
|
+
height;
|
|
1870
|
+
#canvas;
|
|
1871
|
+
constructor(width, height) {
|
|
1872
|
+
this.width = width;
|
|
1873
|
+
this.height = height;
|
|
1874
|
+
this.#canvas = createCanvas(width, height);
|
|
1875
|
+
}
|
|
1876
|
+
getContext(type) {
|
|
1877
|
+
if (type !== "2d")
|
|
1878
|
+
return null;
|
|
1879
|
+
return this.#canvas.getContext("2d");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
globalThis.OffscreenCanvas = BunOffscreenCanvas;
|
|
1883
|
+
initialized = true;
|
|
1884
|
+
}
|
|
1885
|
+
} catch {
|
|
1886
|
+
console.warn("Pretext requires OffscreenCanvas or @napi-rs/canvas for text measurement.");
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/core/image-layout.ts
|
|
1891
|
+
function computeSafePresetY(indexOnPage, height, captionH, stageHeight) {
|
|
1892
|
+
const topTextBuffer = Math.min(260, Math.round(stageHeight * 0.22));
|
|
1893
|
+
const rawY = topTextBuffer + indexOnPage * (height + 32);
|
|
1894
|
+
const maxY = Math.max(60, stageHeight - height - captionH - 16);
|
|
1895
|
+
return Math.min(rawY, maxY);
|
|
1896
|
+
}
|
|
1897
|
+
function resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight) {
|
|
1898
|
+
if (img.templateAssetKey && img.x != null && img.y != null && img.width != null && img.height != null) {
|
|
1899
|
+
return { ...img, x: img.x, y: img.y, width: img.width, height: img.height };
|
|
1900
|
+
}
|
|
1901
|
+
const preset = img.layoutPreset || "auto";
|
|
1902
|
+
const padding = 16;
|
|
1903
|
+
const captionH = img.captionHeight ?? 36;
|
|
1904
|
+
if (img.isAbsolute) {
|
|
1905
|
+
return {
|
|
1906
|
+
...img,
|
|
1907
|
+
x: img.x ?? padding,
|
|
1908
|
+
y: img.y ?? 100,
|
|
1909
|
+
width: img.width ?? 420,
|
|
1910
|
+
height: img.height ?? 280
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
if (preset === "custom") {
|
|
1914
|
+
return {
|
|
1915
|
+
...img,
|
|
1916
|
+
x: img.x ?? padding,
|
|
1917
|
+
y: img.y ?? 260,
|
|
1918
|
+
width: img.width ?? 420,
|
|
1919
|
+
height: img.height ?? 280
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
if (preset === "fill") {
|
|
1923
|
+
const width2 = stageWidth;
|
|
1924
|
+
const height2 = img.height || Math.round(stageHeight * 0.28);
|
|
1925
|
+
const y = Math.max(160, stageHeight - height2 - captionH - padding);
|
|
1926
|
+
return {
|
|
1927
|
+
...img,
|
|
1928
|
+
x: 0,
|
|
1929
|
+
y,
|
|
1930
|
+
width: width2,
|
|
1931
|
+
height: height2
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
if (preset === "corner-soft") {
|
|
1935
|
+
const cornerWidth = Math.min(img.width || 380, Math.round(stageWidth * 0.48));
|
|
1936
|
+
const cornerHeight = img.height || 260;
|
|
1937
|
+
const x = Math.max(0, stageWidth - cornerWidth - padding);
|
|
1938
|
+
const y = Math.max(160, stageHeight - cornerHeight - captionH - 24);
|
|
1939
|
+
return {
|
|
1940
|
+
...img,
|
|
1941
|
+
x,
|
|
1942
|
+
y,
|
|
1943
|
+
width: cornerWidth,
|
|
1944
|
+
height: cornerHeight
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const defaultWidth = 420;
|
|
1948
|
+
const defaultHeight = 280;
|
|
1949
|
+
const maxFloatWidth = Math.round(stageWidth * 0.52);
|
|
1950
|
+
const width = Math.min(img.width || defaultWidth, maxFloatWidth);
|
|
1951
|
+
const height = img.height || defaultHeight;
|
|
1952
|
+
const safeY = computeSafePresetY(indexOnPage, height, captionH, stageHeight);
|
|
1953
|
+
if (preset === "center") {
|
|
1954
|
+
const centerWidth = Math.min(img.width || 560, stageWidth - padding * 2);
|
|
1955
|
+
const x = Math.max(0, Math.round((stageWidth - centerWidth) / 2));
|
|
1956
|
+
return {
|
|
1957
|
+
...img,
|
|
1958
|
+
x,
|
|
1959
|
+
y: safeY,
|
|
1960
|
+
width: centerWidth,
|
|
1961
|
+
height
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
if (preset === "staggered") {
|
|
1965
|
+
const isRight = indexOnPage % 2 === 1;
|
|
1966
|
+
const x = isRight ? Math.max(0, stageWidth - width - padding) : padding;
|
|
1967
|
+
return {
|
|
1968
|
+
...img,
|
|
1969
|
+
x,
|
|
1970
|
+
y: safeY,
|
|
1971
|
+
width,
|
|
1972
|
+
height
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (preset === "mid-right") {
|
|
1976
|
+
const x = Math.max(0, stageWidth - width - padding);
|
|
1977
|
+
return {
|
|
1978
|
+
...img,
|
|
1979
|
+
x,
|
|
1980
|
+
y: safeY,
|
|
1981
|
+
width,
|
|
1982
|
+
height
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
if (preset === "mid-left") {
|
|
1986
|
+
const x = padding;
|
|
1987
|
+
return {
|
|
1988
|
+
...img,
|
|
1989
|
+
x,
|
|
1990
|
+
y: safeY,
|
|
1991
|
+
width,
|
|
1992
|
+
height
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
const isWide = (img.width || defaultWidth) >= stageWidth * 0.75;
|
|
1996
|
+
if (isWide) {
|
|
1997
|
+
const centerWidth = Math.min(img.width || defaultWidth, stageWidth - padding * 2);
|
|
1998
|
+
return {
|
|
1999
|
+
...img,
|
|
2000
|
+
x: Math.max(0, Math.round((stageWidth - centerWidth) / 2)),
|
|
2001
|
+
y: safeY,
|
|
2002
|
+
width: centerWidth,
|
|
2003
|
+
height
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
const autoRight = indexOnPage % 2 === 1;
|
|
2007
|
+
const autoX = autoRight ? Math.max(0, stageWidth - width - padding) : padding;
|
|
2008
|
+
return {
|
|
2009
|
+
...img,
|
|
2010
|
+
x: autoX,
|
|
2011
|
+
y: safeY,
|
|
2012
|
+
width,
|
|
2013
|
+
height
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/core/obstacle-flow.ts
|
|
2018
|
+
var START = { itemIndex: 0, segmentIndex: 0, graphemeIndex: 0 };
|
|
2019
|
+
function runFont(font, run) {
|
|
2020
|
+
const weighted = run.bold ? font.replace(/^\d+\s+/, "700 ") : font;
|
|
2021
|
+
return run.italic ? "italic " + weighted : weighted;
|
|
2022
|
+
}
|
|
2023
|
+
function compareCursor(a, b) {
|
|
2024
|
+
return a.itemIndex - b.itemIndex || a.segmentIndex - b.segmentIndex || a.graphemeIndex - b.graphemeIndex;
|
|
2025
|
+
}
|
|
2026
|
+
function paginateBlocks(blocks, stageWidth, stageHeight, allImages, options = {}) {
|
|
2027
|
+
ensurePretextRuntime();
|
|
2028
|
+
let textHeight = stageHeight;
|
|
2029
|
+
const gap = options.obstacleGap ?? 24;
|
|
2030
|
+
const imageLimit = options.pageImageLimit ?? 2;
|
|
2031
|
+
const prepared = blocks.map((block) => {
|
|
2032
|
+
const runs = block.runs.map((run) => ({
|
|
2033
|
+
...run,
|
|
2034
|
+
font: runFont(block.font, run)
|
|
2035
|
+
}));
|
|
2036
|
+
return {
|
|
2037
|
+
...block,
|
|
2038
|
+
runs,
|
|
2039
|
+
prepared: prepareRichInline(runs)
|
|
2040
|
+
};
|
|
2041
|
+
});
|
|
2042
|
+
function place(block, cursor2, initialY, images, limit = Infinity) {
|
|
2043
|
+
const lines = [];
|
|
2044
|
+
let y = initialY;
|
|
2045
|
+
while (y + block.lineHeight <= textHeight && lines.length < limit) {
|
|
2046
|
+
if (!layoutNextRichInlineLineRange(block.prepared, stageWidth, cursor2)) {
|
|
2047
|
+
return { lines, cursor: cursor2, y, done: true };
|
|
2048
|
+
}
|
|
2049
|
+
const indent = block.textIndent ?? 0;
|
|
2050
|
+
let intervals = [{ left: 0, right: stageWidth }];
|
|
2051
|
+
const blockers = images.filter((img) => !img.isAbsolute && y + block.lineHeight > img.y - gap && y < img.y + img.height + (img.captionHeight ?? 0) + gap);
|
|
2052
|
+
for (const img of blockers) {
|
|
2053
|
+
intervals = intervals.flatMap((interval) => {
|
|
2054
|
+
if (img.x - gap >= interval.right || img.x + img.width + gap <= interval.left) {
|
|
2055
|
+
return [interval];
|
|
2056
|
+
}
|
|
2057
|
+
return [
|
|
2058
|
+
{ left: interval.left, right: Math.min(interval.right, img.x - gap) },
|
|
2059
|
+
{ left: Math.max(interval.left, img.x + img.width + gap), right: interval.right }
|
|
2060
|
+
];
|
|
2061
|
+
}).filter((interval) => interval.right > interval.left);
|
|
2062
|
+
}
|
|
2063
|
+
const fontSize = Number(block.font.match(/(\d+(?:\.\d+)?)px/)?.[1] ?? 36);
|
|
2064
|
+
let best = null;
|
|
2065
|
+
for (const interval of intervals) {
|
|
2066
|
+
const slotWidth = interval.right - interval.left - indent;
|
|
2067
|
+
if (slotWidth < fontSize * (blockers.length ? 2.5 : 1))
|
|
2068
|
+
continue;
|
|
2069
|
+
const line = layoutNextRichInlineLineRange(block.prepared, slotWidth, cursor2);
|
|
2070
|
+
if (!line || line.width > slotWidth + 0.5)
|
|
2071
|
+
continue;
|
|
2072
|
+
if (!best || compareCursor(line.end, best.line.end) > 0) {
|
|
2073
|
+
best = { line, left: interval.left + indent, width: slotWidth };
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (!best) {
|
|
2077
|
+
if (!blockers.length) {
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
y = Math.min(...blockers.map((img) => img.y + img.height + (img.captionHeight ?? 0) + gap));
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
if (compareCursor(best.line.end, cursor2) <= 0) {
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
const lineData = materializeRichInlineLineRange(block.prepared, best.line);
|
|
2087
|
+
const fragments = lineData.fragments.map((frag) => {
|
|
2088
|
+
const originalRun = block.runs[frag.itemIndex];
|
|
2089
|
+
return {
|
|
2090
|
+
text: frag.text,
|
|
2091
|
+
font: originalRun.font ?? block.font,
|
|
2092
|
+
bold: originalRun.bold,
|
|
2093
|
+
italic: originalRun.italic,
|
|
2094
|
+
highlight: originalRun.highlight,
|
|
2095
|
+
gapBefore: frag.gapBefore,
|
|
2096
|
+
occupiedWidth: frag.occupiedWidth,
|
|
2097
|
+
letterSpacing: originalRun.letterSpacing
|
|
2098
|
+
};
|
|
2099
|
+
});
|
|
2100
|
+
let lineX = best.left;
|
|
2101
|
+
let bulletX = compareCursor(cursor2, START) === 0 && block.bullet ? best.left - indent : undefined;
|
|
2102
|
+
const occupied = best.line.width;
|
|
2103
|
+
if (options.textAlign === "center") {
|
|
2104
|
+
const free = Math.max(0, best.width - occupied);
|
|
2105
|
+
const shift = Math.round(free / 2);
|
|
2106
|
+
lineX += shift;
|
|
2107
|
+
if (bulletX !== undefined)
|
|
2108
|
+
bulletX += shift;
|
|
2109
|
+
} else if (options.textAlign === "right") {
|
|
2110
|
+
const free = Math.max(0, best.width - occupied);
|
|
2111
|
+
lineX += free;
|
|
2112
|
+
if (bulletX !== undefined)
|
|
2113
|
+
bulletX += free;
|
|
2114
|
+
}
|
|
2115
|
+
lines.push({
|
|
2116
|
+
text: fragments.map((f) => f.text).join(""),
|
|
2117
|
+
fragments,
|
|
2118
|
+
x: lineX,
|
|
2119
|
+
y,
|
|
2120
|
+
className: block.className,
|
|
2121
|
+
font: block.font,
|
|
2122
|
+
lineHeight: block.lineHeight,
|
|
2123
|
+
maxWidth: best.width,
|
|
2124
|
+
start: cursor2,
|
|
2125
|
+
end: best.line.end,
|
|
2126
|
+
bullet: compareCursor(cursor2, START) === 0 ? block.bullet : undefined,
|
|
2127
|
+
bulletX
|
|
2128
|
+
});
|
|
2129
|
+
cursor2 = best.line.end;
|
|
2130
|
+
y += block.lineHeight;
|
|
2131
|
+
}
|
|
2132
|
+
return {
|
|
2133
|
+
lines,
|
|
2134
|
+
cursor: cursor2,
|
|
2135
|
+
y,
|
|
2136
|
+
done: layoutNextRichInlineLineRange(block.prepared, stageWidth, cursor2) === null
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const pages = [];
|
|
2140
|
+
let blockIndex = 0;
|
|
2141
|
+
let cursor = START;
|
|
2142
|
+
const initialPageNumber = options.initialPageNumber ?? 1;
|
|
2143
|
+
const isSegmentMode = options.initialPageNumber !== undefined;
|
|
2144
|
+
let unassignedIndex = 0;
|
|
2145
|
+
const unassignedImages = allImages.filter((img) => !img.pageIndex);
|
|
2146
|
+
let segmentImagesPlaced = false;
|
|
2147
|
+
const MAX_PAGES = options.maxPages || 50;
|
|
2148
|
+
const templateImages = options.templateImages || [];
|
|
2149
|
+
const lastTemplatePage = Math.max(0, ...templateImages.filter((image) => !image.repeatEveryPage).map((image) => image.pageIndex || 1));
|
|
2150
|
+
const lastPageNumber = Math.max(options.lastPageNumber || 0, isSegmentMode ? 0 : lastTemplatePage);
|
|
2151
|
+
const templatesForPage = (page) => templateImages.filter((image) => image.repeatEveryPage || (image.pageIndex || 1) === page);
|
|
2152
|
+
const lastImagePage = isSegmentMode ? 0 : Math.max(0, ...allImages.map((image) => image.pageIndex || 0));
|
|
2153
|
+
const hasRemaining = () => blockIndex < prepared.length || !isSegmentMode && (unassignedIndex < unassignedImages.length || initialPageNumber + pages.length <= lastImagePage) || isSegmentMode && !segmentImagesPlaced && (allImages.length > 0 || templatesForPage(initialPageNumber).length > 0) || initialPageNumber + pages.length <= lastPageNumber;
|
|
2154
|
+
while (pages.length < MAX_PAGES && hasRemaining()) {
|
|
2155
|
+
const pageNum = initialPageNumber + pages.length;
|
|
2156
|
+
const targetFill = options.pageFillRatios?.[pageNum] ?? options.targetFillRatio ?? 1;
|
|
2157
|
+
if (!Number.isFinite(targetFill) || targetFill <= 0 || targetFill > 1) {
|
|
2158
|
+
throw new RangeError("targetFillRatio must be greater than 0 and at most 1");
|
|
2159
|
+
}
|
|
2160
|
+
textHeight = stageHeight * targetFill;
|
|
2161
|
+
let targetImages = [];
|
|
2162
|
+
if (isSegmentMode) {
|
|
2163
|
+
if (!segmentImagesPlaced) {
|
|
2164
|
+
targetImages = allImages;
|
|
2165
|
+
segmentImagesPlaced = true;
|
|
2166
|
+
}
|
|
2167
|
+
} else {
|
|
2168
|
+
const pageExplicitImages = allImages.filter((img) => img.pageIndex === pageNum);
|
|
2169
|
+
if (pageExplicitImages.length > 0) {
|
|
2170
|
+
targetImages = pageExplicitImages;
|
|
2171
|
+
} else {
|
|
2172
|
+
targetImages = unassignedImages.slice(unassignedIndex, unassignedIndex + imageLimit);
|
|
2173
|
+
unassignedIndex += Math.max(targetImages.length, 1);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const images = [...targetImages, ...templatesForPage(pageNum)].map((img, idx) => resolveImagePlacement(img, idx, stageWidth, stageHeight));
|
|
2177
|
+
const lines = [];
|
|
2178
|
+
let y = 0;
|
|
2179
|
+
const initialBlockIndex = blockIndex;
|
|
2180
|
+
const initialCursor = cursor;
|
|
2181
|
+
while (blockIndex < prepared.length) {
|
|
2182
|
+
const block = prepared[blockIndex];
|
|
2183
|
+
const startsBlock = compareCursor(cursor, START) === 0;
|
|
2184
|
+
const startY = y + (startsBlock && lines.length ? block.gapBefore ?? 0 : 0);
|
|
2185
|
+
if (startsBlock && block.keepWithNext && lines.length) {
|
|
2186
|
+
let probeY = startY;
|
|
2187
|
+
let fits = true;
|
|
2188
|
+
for (let idx = blockIndex;idx < prepared.length; idx++) {
|
|
2189
|
+
const f = prepared[idx];
|
|
2190
|
+
if (idx > blockIndex)
|
|
2191
|
+
probeY += f.gapBefore ?? 0;
|
|
2192
|
+
const probe = place(f, START, probeY, images, f.keepWithNext ? Infinity : 2);
|
|
2193
|
+
if (f.keepWithNext ? !probe.done : probe.lines.length < 2 && !probe.done) {
|
|
2194
|
+
fits = false;
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
if (!f.keepWithNext)
|
|
2198
|
+
break;
|
|
2199
|
+
probeY = probe.y + (f.gapAfter ?? 0);
|
|
2200
|
+
}
|
|
2201
|
+
if (!fits)
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
const chunk = place(block, cursor, startY, images);
|
|
2205
|
+
if (!chunk.done && chunk.lines.length) {
|
|
2206
|
+
const remainder = place(block, chunk.cursor, 0, [], 2);
|
|
2207
|
+
if (remainder.done && remainder.lines.length === 1 && chunk.lines.length >= 2) {
|
|
2208
|
+
const moved = chunk.lines.pop();
|
|
2209
|
+
chunk.cursor = moved.start;
|
|
2210
|
+
chunk.y = moved.y;
|
|
2211
|
+
}
|
|
2212
|
+
if (startsBlock && chunk.lines.length < 2 && lines.length) {
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
lines.push(...chunk.lines);
|
|
2217
|
+
cursor = chunk.cursor;
|
|
2218
|
+
y = chunk.y;
|
|
2219
|
+
if (!chunk.done)
|
|
2220
|
+
break;
|
|
2221
|
+
blockIndex++;
|
|
2222
|
+
cursor = START;
|
|
2223
|
+
y += block.gapAfter ?? 0;
|
|
2224
|
+
}
|
|
2225
|
+
if (blockIndex === initialBlockIndex && compareCursor(cursor, initialCursor) === 0 && !targetImages.length && !images.some((image) => !image.isAbsolute && !image.repeatEveryPage) && blockIndex < prepared.length) {
|
|
2226
|
+
throw new RangeError("Text cannot fit in the available page area; reduce font size or enlarge the safe area");
|
|
2227
|
+
}
|
|
2228
|
+
const textBottom = lines.reduce((max, l) => Math.max(max, l.y + l.lineHeight), 0);
|
|
2229
|
+
const flowImages = images.filter((img) => !img.isAbsolute);
|
|
2230
|
+
const imageBottom = flowImages.reduce((max, img) => Math.max(max, img.y + img.height + (img.captionHeight ?? 0)), 0);
|
|
2231
|
+
const contentBottom = Math.max(textBottom, imageBottom);
|
|
2232
|
+
const fillRatio = stageHeight > 0 ? Math.min(1, contentBottom / stageHeight) : 0;
|
|
2233
|
+
if (lines.length > 0 && options.verticalAlign && options.verticalAlign !== "top") {
|
|
2234
|
+
const minTextY = lines.reduce((min, l) => Math.min(min, l.y), Infinity);
|
|
2235
|
+
const minImageY = flowImages.reduce((min, img) => Math.min(min, img.y), Infinity);
|
|
2236
|
+
const contentTop = Math.min(minTextY, minImageY === Infinity ? minTextY : minImageY);
|
|
2237
|
+
const contentSpan = contentBottom - contentTop;
|
|
2238
|
+
if (stageHeight > contentSpan) {
|
|
2239
|
+
let offsetY = 0;
|
|
2240
|
+
if (options.verticalAlign === "center") {
|
|
2241
|
+
const targetTop = Math.round((stageHeight - contentSpan) / 2);
|
|
2242
|
+
offsetY = targetTop - contentTop;
|
|
2243
|
+
} else if (options.verticalAlign === "bottom") {
|
|
2244
|
+
const targetTop = stageHeight - contentSpan;
|
|
2245
|
+
offsetY = targetTop - contentTop;
|
|
2246
|
+
}
|
|
2247
|
+
if (offsetY !== 0) {
|
|
2248
|
+
for (const line of lines) {
|
|
2249
|
+
line.y += offsetY;
|
|
2250
|
+
}
|
|
2251
|
+
for (const img of flowImages) {
|
|
2252
|
+
if (img.layoutPreset !== "custom") {
|
|
2253
|
+
img.y += offsetY;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
pages.push({
|
|
2260
|
+
pageNumber: pageNum,
|
|
2261
|
+
lines,
|
|
2262
|
+
images,
|
|
2263
|
+
textBottom,
|
|
2264
|
+
fillRatio
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
if (hasRemaining())
|
|
2268
|
+
throw new RangeError(`Content exceeds maxPages=${MAX_PAGES}; no content was discarded`);
|
|
2269
|
+
return pages;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/core/typography.ts
|
|
2273
|
+
function modularScale(base, ratio, step) {
|
|
2274
|
+
return Math.round(base * Math.pow(ratio, step));
|
|
2275
|
+
}
|
|
2276
|
+
function calculateLineHeight(fontSizePx, userMultiplier = 1.45) {
|
|
2277
|
+
if (fontSizePx <= 0)
|
|
2278
|
+
return 24;
|
|
2279
|
+
const logNorm = Math.log(Math.max(8, fontSizePx) / 8) / Math.log(160 / 8);
|
|
2280
|
+
const baseCurve = 1.65 - logNorm * (1.65 - 1.15);
|
|
2281
|
+
const ratio = Math.max(1.1, Math.min(1.8, baseCurve * (userMultiplier / 1.45)));
|
|
2282
|
+
return Math.round(fontSizePx * ratio);
|
|
2283
|
+
}
|
|
2284
|
+
function resolveFontFamilyStack(fontName, isSerif = false) {
|
|
2285
|
+
const trimmed = (fontName || "").trim();
|
|
2286
|
+
switch (trimmed) {
|
|
2287
|
+
case "PingFang SC":
|
|
2288
|
+
return `"PingFang SC", -apple-system, BlinkMacSystemFont, "Noto Sans SC", "Microsoft YaHei", sans-serif`;
|
|
2289
|
+
case "Noto Sans SC":
|
|
2290
|
+
return `"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif`;
|
|
2291
|
+
case "Songti SC":
|
|
2292
|
+
return `"Songti SC", "Noto Serif SC", "STSong", "SimSun", serif`;
|
|
2293
|
+
case "Noto Serif SC":
|
|
2294
|
+
return `"Noto Serif SC", "Songti SC", "SimSun", serif`;
|
|
2295
|
+
case "Kaiti SC":
|
|
2296
|
+
return `"Kaiti SC", "LXGW WenKai Screen", "STKaiti", "KaiTi", serif`;
|
|
2297
|
+
case "Yuanti SC":
|
|
2298
|
+
return `"Yuanti SC", "STYuanti", "PingFang SC", sans-serif`;
|
|
2299
|
+
case "Inter":
|
|
2300
|
+
return `Inter, "PingFang SC", system-ui, sans-serif`;
|
|
2301
|
+
case "Georgia":
|
|
2302
|
+
return `Georgia, "Songti SC", serif`;
|
|
2303
|
+
default:
|
|
2304
|
+
if (!trimmed) {
|
|
2305
|
+
return isSerif ? '"Songti SC", serif' : '"PingFang SC", sans-serif';
|
|
2306
|
+
}
|
|
2307
|
+
if (trimmed.includes(","))
|
|
2308
|
+
return trimmed;
|
|
2309
|
+
return `"${trimmed}", ${isSerif ? "serif" : "sans-serif"}`;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function computeBlockStyles(params) {
|
|
2313
|
+
const { baseFontSize, scaleRatio, lineHeightMultiplier, headingFont, bodyFont } = params;
|
|
2314
|
+
const resolvedHeadingFont = resolveFontFamilyStack(headingFont, false);
|
|
2315
|
+
const resolvedBodyFont = resolveFontFamilyStack(bodyFont, true);
|
|
2316
|
+
const pSize = baseFontSize;
|
|
2317
|
+
const h3Size = modularScale(baseFontSize, scaleRatio, 1);
|
|
2318
|
+
const h2Size = modularScale(baseFontSize, scaleRatio, 2);
|
|
2319
|
+
const h1Size = modularScale(baseFontSize, scaleRatio, 3);
|
|
2320
|
+
const quoteSize = baseFontSize;
|
|
2321
|
+
const listSize = baseFontSize;
|
|
2322
|
+
return {
|
|
2323
|
+
heading: {
|
|
2324
|
+
font: `700 ${h1Size}px ${resolvedHeadingFont}`,
|
|
2325
|
+
fontSize: h1Size,
|
|
2326
|
+
lineHeight: calculateLineHeight(h1Size, lineHeightMultiplier * 0.95),
|
|
2327
|
+
gapBefore: Math.round(h1Size * 0.8),
|
|
2328
|
+
gapAfter: Math.round(h1Size * 0.4),
|
|
2329
|
+
className: "body-heading"
|
|
2330
|
+
},
|
|
2331
|
+
subheading: {
|
|
2332
|
+
font: `700 ${h2Size}px ${resolvedHeadingFont}`,
|
|
2333
|
+
fontSize: h2Size,
|
|
2334
|
+
lineHeight: calculateLineHeight(h2Size, lineHeightMultiplier * 0.98),
|
|
2335
|
+
gapBefore: Math.round(h2Size * 0.7),
|
|
2336
|
+
gapAfter: Math.round(h2Size * 0.35),
|
|
2337
|
+
className: "body-subheading"
|
|
2338
|
+
},
|
|
2339
|
+
"minor-heading": {
|
|
2340
|
+
font: `700 ${h3Size}px ${resolvedHeadingFont}`,
|
|
2341
|
+
fontSize: h3Size,
|
|
2342
|
+
lineHeight: calculateLineHeight(h3Size, lineHeightMultiplier),
|
|
2343
|
+
gapBefore: Math.round(h3Size * 0.6),
|
|
2344
|
+
gapAfter: Math.round(h3Size * 0.3),
|
|
2345
|
+
className: "body-minor-heading"
|
|
2346
|
+
},
|
|
2347
|
+
paragraph: {
|
|
2348
|
+
font: `400 ${pSize}px ${resolvedBodyFont}`,
|
|
2349
|
+
fontSize: pSize,
|
|
2350
|
+
lineHeight: calculateLineHeight(pSize, lineHeightMultiplier),
|
|
2351
|
+
gapBefore: 0,
|
|
2352
|
+
gapAfter: Math.round(pSize * 0.6),
|
|
2353
|
+
className: "body-line"
|
|
2354
|
+
},
|
|
2355
|
+
quote: {
|
|
2356
|
+
font: `400 ${quoteSize}px ${resolvedBodyFont}`,
|
|
2357
|
+
fontSize: quoteSize,
|
|
2358
|
+
lineHeight: calculateLineHeight(quoteSize, lineHeightMultiplier * 1.05),
|
|
2359
|
+
gapBefore: Math.round(quoteSize * 0.5),
|
|
2360
|
+
gapAfter: Math.round(quoteSize * 0.5),
|
|
2361
|
+
className: "body-quote"
|
|
2362
|
+
},
|
|
2363
|
+
"list-item": {
|
|
2364
|
+
font: `400 ${listSize}px ${resolvedBodyFont}`,
|
|
2365
|
+
fontSize: listSize,
|
|
2366
|
+
lineHeight: calculateLineHeight(listSize, lineHeightMultiplier),
|
|
2367
|
+
gapBefore: 0,
|
|
2368
|
+
gapAfter: Math.round(listSize * 0.3),
|
|
2369
|
+
className: "body-line"
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// src/core/template.ts
|
|
2375
|
+
function validateTemplateAssets(images = []) {
|
|
2376
|
+
if (!Array.isArray(images))
|
|
2377
|
+
throw new RangeError("模板图片必须是数组");
|
|
2378
|
+
const keys = new Set;
|
|
2379
|
+
for (const image of images) {
|
|
2380
|
+
if (!image || typeof image !== "object")
|
|
2381
|
+
throw new RangeError("无效的模板图片");
|
|
2382
|
+
if (image.templateAssetKey === undefined)
|
|
2383
|
+
continue;
|
|
2384
|
+
const key = image.templateAssetKey;
|
|
2385
|
+
if (image.repeatEveryPage !== undefined && typeof image.repeatEveryPage !== "boolean")
|
|
2386
|
+
throw new RangeError(`素材 ${key} 的每页重复开关必须是布尔值`);
|
|
2387
|
+
if (typeof key !== "string" || !/^[a-zA-Z0-9_-]+$/.test(key)) {
|
|
2388
|
+
throw new RangeError("素材标识只能包含字母、数字、下划线和短横线");
|
|
2389
|
+
}
|
|
2390
|
+
if (image.repeatEveryPage && !image.isAbsolute)
|
|
2391
|
+
throw new RangeError(`素材 ${key} 的每页重复仅支持自由定位模式`);
|
|
2392
|
+
if (keys.has(key))
|
|
2393
|
+
throw new RangeError(`重复的模板素材标识:${key}`);
|
|
2394
|
+
keys.add(key);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function applyTemplateImages(currentImages, templateImages = [], overrides = {}) {
|
|
2398
|
+
validateTemplateAssets(templateImages);
|
|
2399
|
+
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides))
|
|
2400
|
+
throw new RangeError("素材替换必须是标识到图片地址的映射");
|
|
2401
|
+
const fixed = templateImages.filter((image) => image.templateAssetKey);
|
|
2402
|
+
for (const [key, src] of Object.entries(overrides)) {
|
|
2403
|
+
if (!fixed.some((image) => image.templateAssetKey === key))
|
|
2404
|
+
throw new RangeError(`未知模板素材:${key}`);
|
|
2405
|
+
if (typeof src !== "string" || !src.trim())
|
|
2406
|
+
throw new RangeError(`素材 ${key} 的替换图片不能为空`);
|
|
2407
|
+
}
|
|
2408
|
+
const content = currentImages.filter((image) => !image.templateAssetKey);
|
|
2409
|
+
const slots = templateImages.filter((image) => !image.templateAssetKey);
|
|
2410
|
+
const mapped = content.length === 0 ? slots.map((image) => ({ ...image, id: `img_${crypto.randomUUID()}` })) : content.map((image, index) => {
|
|
2411
|
+
const slot = slots[index];
|
|
2412
|
+
if (!slot)
|
|
2413
|
+
return image;
|
|
2414
|
+
return {
|
|
2415
|
+
...image,
|
|
2416
|
+
layoutPreset: slot.layoutPreset ?? image.layoutPreset,
|
|
2417
|
+
isAbsolute: slot.isAbsolute ?? image.isAbsolute,
|
|
2418
|
+
x: slot.x ?? image.x,
|
|
2419
|
+
y: slot.y ?? image.y,
|
|
2420
|
+
width: slot.width ?? image.width,
|
|
2421
|
+
height: slot.height ?? image.height,
|
|
2422
|
+
rotate: slot.rotate ?? image.rotate,
|
|
2423
|
+
opacity: slot.opacity ?? image.opacity,
|
|
2424
|
+
zIndex: slot.zIndex ?? image.zIndex,
|
|
2425
|
+
borderRadius: slot.borderRadius ?? image.borderRadius,
|
|
2426
|
+
captionHeight: slot.captionHeight ?? image.captionHeight,
|
|
2427
|
+
pageIndex: slot.pageIndex ?? image.pageIndex
|
|
2428
|
+
};
|
|
2429
|
+
});
|
|
2430
|
+
return [...mapped, ...fixed.map((image) => ({
|
|
2431
|
+
...image,
|
|
2432
|
+
src: Object.hasOwn(overrides, image.templateAssetKey) ? overrides[image.templateAssetKey] : image.src
|
|
2433
|
+
}))];
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/core/engine.ts
|
|
2437
|
+
function getDefaultConfig(aspectRatio = "longform-3-4") {
|
|
2438
|
+
const spec = getCanvasSpec(aspectRatio);
|
|
2439
|
+
const isCover = aspectRatio === "wechat-cover-2.35";
|
|
2440
|
+
const isSquare = aspectRatio === "square-1-1";
|
|
2441
|
+
const baseSize = aspectRatio === "wechat-cover-2.35" ? 56 : aspectRatio === "wide-16-9" ? 44 : aspectRatio === "square-1-1" ? 48 : 40;
|
|
2442
|
+
return {
|
|
2443
|
+
name: "默认样式",
|
|
2444
|
+
aspectRatio,
|
|
2445
|
+
algorithm: {
|
|
2446
|
+
baseFontSize: baseSize,
|
|
2447
|
+
scaleRatio: isCover ? 1.35 : 1.25,
|
|
2448
|
+
lineHeightMultiplier: 1.45,
|
|
2449
|
+
obstacleGap: 24,
|
|
2450
|
+
targetFillRatio: 0.8,
|
|
2451
|
+
contentBottomGap: isCover ? 0 : 20,
|
|
2452
|
+
textAlign: isCover || isSquare ? "center" : "left",
|
|
2453
|
+
verticalAlign: isCover || isSquare ? "center" : "top",
|
|
2454
|
+
safeArea: isCover ? { top: 40, right: 60, bottom: 40, left: 60 } : spec.defaultSafeArea
|
|
2455
|
+
},
|
|
2456
|
+
theme: {
|
|
2457
|
+
bgColor: "#ffffff",
|
|
2458
|
+
textColor: "#171717",
|
|
2459
|
+
headingColor: "#171717",
|
|
2460
|
+
accentColor: "#059669",
|
|
2461
|
+
quoteColor: "#525252",
|
|
2462
|
+
captionColor: "#737373",
|
|
2463
|
+
watermarkColor: "#a3a3a3",
|
|
2464
|
+
cardBg: "#f5f5f5",
|
|
2465
|
+
borderSubtle: "#e5e5e5"
|
|
2466
|
+
},
|
|
2467
|
+
typography: {
|
|
2468
|
+
headingFont: "PingFang SC",
|
|
2469
|
+
bodyFont: "Songti SC"
|
|
2470
|
+
},
|
|
2471
|
+
footerText: isCover ? "" : "Typeset Studio · 纯算法排版",
|
|
2472
|
+
watermarkText: isCover ? "" : "@Antigravity",
|
|
2473
|
+
chrome: {
|
|
2474
|
+
showHeader: !isCover,
|
|
2475
|
+
showWatermark: !isCover,
|
|
2476
|
+
showPageNumber: !isCover && !isSquare,
|
|
2477
|
+
showFooter: !isCover,
|
|
2478
|
+
showFooterDivider: !isCover,
|
|
2479
|
+
showFooterText: !isCover,
|
|
2480
|
+
showFillRatio: false
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
function runTypeset(params) {
|
|
2485
|
+
const { markdown, images = [], config, highlightWords = [], minPages = 1, maxPages = 0, pageFillRatios } = params;
|
|
2486
|
+
if (!Number.isInteger(minPages) || minPages < 1 || !Number.isInteger(maxPages) || maxPages < 0 || maxPages > 0 && maxPages < minPages) {
|
|
2487
|
+
throw new RangeError("Invalid page constraints: maxPages must be 0 or at least minPages");
|
|
2488
|
+
}
|
|
2489
|
+
validateTemplateAssets(images);
|
|
2490
|
+
if (images.some((image) => !image.repeatEveryPage && image.pageIndex != null && (!Number.isInteger(image.pageIndex) || image.pageIndex < 1 || image.pageIndex > 50))) {
|
|
2491
|
+
throw new RangeError("Image pageIndex must be an integer between 1 and 50");
|
|
2492
|
+
}
|
|
2493
|
+
const spec = getCanvasSpec(config.aspectRatio);
|
|
2494
|
+
const { stageWidth, stageHeight } = computeStageDimensions(spec, config.algorithm.safeArea, config.algorithm.contentBottomGap);
|
|
2495
|
+
const blockStyles = computeBlockStyles({
|
|
2496
|
+
baseFontSize: config.algorithm.baseFontSize,
|
|
2497
|
+
scaleRatio: config.algorithm.scaleRatio,
|
|
2498
|
+
lineHeightMultiplier: config.algorithm.lineHeightMultiplier,
|
|
2499
|
+
headingFont: config.typography.headingFont,
|
|
2500
|
+
bodyFont: config.typography.bodyFont
|
|
2501
|
+
});
|
|
2502
|
+
if (config.coverTheme) {
|
|
2503
|
+
const fonts = config.coverTheme.typography;
|
|
2504
|
+
for (const [kind, style] of Object.entries(blockStyles)) {
|
|
2505
|
+
const font = kind.includes("heading") ? fonts.title : fonts.subtitle;
|
|
2506
|
+
Object.assign(style, {
|
|
2507
|
+
font: `${font.fontWeight} ${font.fontSize}px "${font.fontFamily}"`,
|
|
2508
|
+
lineHeight: Math.round(font.fontSize * font.lineHeight)
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const createFlowBlocks = (text) => parseContentBlocks(text).map((block) => ({
|
|
2513
|
+
...blockStyles[block.kind],
|
|
2514
|
+
kind: block.kind,
|
|
2515
|
+
text: block.text,
|
|
2516
|
+
runs: parseInlineRuns(block.text, highlightWords).map((run) => ({
|
|
2517
|
+
...run,
|
|
2518
|
+
letterSpacing: config.coverTheme?.typography[block.kind.includes("heading") ? "title" : "subtitle"].letterSpacing
|
|
2519
|
+
})),
|
|
2520
|
+
bullet: block.kind === "list-item" ? block.bullet ?? "•" : undefined,
|
|
2521
|
+
textIndent: block.kind === "list-item" ? 28 : block.kind === "quote" ? 20 : 0,
|
|
2522
|
+
keepWithNext: ["heading", "subheading", "minor-heading"].includes(block.kind)
|
|
2523
|
+
}));
|
|
2524
|
+
const templateImages = images.filter((image) => image.templateAssetKey);
|
|
2525
|
+
const contentImages = images.filter((image) => !image.templateAssetKey);
|
|
2526
|
+
const pageSegments = splitTextByPages(markdown);
|
|
2527
|
+
let pages = [];
|
|
2528
|
+
if (pageSegments.length > 1) {
|
|
2529
|
+
const segmentCount = Math.max(pageSegments.length, ...contentImages.map((image) => image.pageIndex || 1));
|
|
2530
|
+
if (segmentCount > 50)
|
|
2531
|
+
throw new RangeError("Content exceeds engine limit of 50 pages");
|
|
2532
|
+
for (let index = 0;index < segmentCount; index++) {
|
|
2533
|
+
const pageImages = contentImages.filter((image) => (image.pageIndex || 1) === index + 1);
|
|
2534
|
+
const subPages = paginateBlocks(createFlowBlocks(pageSegments[index] || ""), stageWidth, stageHeight, pageImages, {
|
|
2535
|
+
obstacleGap: config.algorithm.obstacleGap,
|
|
2536
|
+
pageImageLimit: 2,
|
|
2537
|
+
textAlign: config.algorithm.textAlign,
|
|
2538
|
+
verticalAlign: config.algorithm.verticalAlign,
|
|
2539
|
+
initialPageNumber: pages.length + 1,
|
|
2540
|
+
templateImages,
|
|
2541
|
+
targetFillRatio: pageFillRatios?.[index + 1] ?? config.algorithm.targetFillRatio
|
|
2542
|
+
});
|
|
2543
|
+
pages.push(...subPages.length ? subPages : [{
|
|
2544
|
+
pageNumber: pages.length + 1,
|
|
2545
|
+
lines: [],
|
|
2546
|
+
images: [],
|
|
2547
|
+
textBottom: 0,
|
|
2548
|
+
fillRatio: 0
|
|
2549
|
+
}]);
|
|
2550
|
+
}
|
|
2551
|
+
const lastTemplatePage = Math.max(0, ...templateImages.filter((image) => !image.repeatEveryPage).map((image) => image.pageIndex || 1));
|
|
2552
|
+
if (pages.length < lastTemplatePage) {
|
|
2553
|
+
pages.push(...paginateBlocks([], stageWidth, stageHeight, [], {
|
|
2554
|
+
initialPageNumber: pages.length + 1,
|
|
2555
|
+
lastPageNumber: lastTemplatePage,
|
|
2556
|
+
templateImages
|
|
2557
|
+
}));
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
const flowBlocks = createFlowBlocks(markdown);
|
|
2561
|
+
pages = paginateBlocks(flowBlocks, stageWidth, stageHeight, contentImages, {
|
|
2562
|
+
obstacleGap: config.algorithm.obstacleGap,
|
|
2563
|
+
pageImageLimit: config.aspectRatio === "longform-3-4" ? 2 : 1,
|
|
2564
|
+
textAlign: config.algorithm.textAlign,
|
|
2565
|
+
verticalAlign: config.algorithm.verticalAlign,
|
|
2566
|
+
targetFillRatio: config.algorithm.targetFillRatio,
|
|
2567
|
+
pageFillRatios,
|
|
2568
|
+
templateImages
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
if (pageSegments.length === 1 && pages.length < minPages && markdown.trim()) {
|
|
2572
|
+
for (let attempt = 1;attempt <= 8 && pages.length < minPages; attempt++) {
|
|
2573
|
+
const ratio = config.algorithm.targetFillRatio * Math.pow(0.75, attempt);
|
|
2574
|
+
try {
|
|
2575
|
+
pages = paginateBlocks(createFlowBlocks(markdown), stageWidth, stageHeight, contentImages, {
|
|
2576
|
+
obstacleGap: config.algorithm.obstacleGap,
|
|
2577
|
+
pageImageLimit: config.aspectRatio === "longform-3-4" ? 2 : 1,
|
|
2578
|
+
textAlign: config.algorithm.textAlign,
|
|
2579
|
+
verticalAlign: config.algorithm.verticalAlign,
|
|
2580
|
+
targetFillRatio: ratio,
|
|
2581
|
+
templateImages,
|
|
2582
|
+
pageFillRatios: pageFillRatios && Object.fromEntries(Object.entries(pageFillRatios).map(([page, fill]) => [page, Math.min(fill, ratio)]))
|
|
2583
|
+
});
|
|
2584
|
+
} catch {
|
|
2585
|
+
break;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (pages.length < minPages && minPages > 1)
|
|
2590
|
+
throw new RangeError(`Content cannot satisfy minPages=${minPages}; lower the minimum page count`);
|
|
2591
|
+
if (pages.length > 50)
|
|
2592
|
+
throw new RangeError("Content exceeds engine limit of 50 pages");
|
|
2593
|
+
if (maxPages > 0 && pages.length > maxPages)
|
|
2594
|
+
throw new RangeError(`Content exceeds maxPages=${maxPages}; no content was discarded`);
|
|
2595
|
+
if (pages.length === 0) {
|
|
2596
|
+
pages = [
|
|
2597
|
+
{
|
|
2598
|
+
pageNumber: 1,
|
|
2599
|
+
lines: [],
|
|
2600
|
+
images: images.filter((image) => !image.templateAssetKey || image.repeatEveryPage || (image.pageIndex || 1) === 1).map((img, idx) => resolveImagePlacement(img, idx, stageWidth, stageHeight)),
|
|
2601
|
+
textBottom: 0,
|
|
2602
|
+
fillRatio: 0
|
|
2603
|
+
}
|
|
2604
|
+
];
|
|
2605
|
+
}
|
|
2606
|
+
return {
|
|
2607
|
+
pages,
|
|
2608
|
+
pageCount: pages.length,
|
|
2609
|
+
canvasSpec: spec,
|
|
2610
|
+
config
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/server/routes/templates.ts
|
|
2615
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2616
|
+
import { dirname, join } from "path";
|
|
2617
|
+
var TEMPLATES_DIR = join(resolveDataDir(), "templates");
|
|
2618
|
+
function ensureTemplatesDir() {
|
|
2619
|
+
if (!existsSync(TEMPLATES_DIR)) {
|
|
2620
|
+
try {
|
|
2621
|
+
mkdirSync(TEMPLATES_DIR, { recursive: true });
|
|
2622
|
+
} catch {}
|
|
2623
|
+
}
|
|
2624
|
+
const seedMarker = join(TEMPLATES_DIR, ".initialized");
|
|
2625
|
+
if (!existsSync(seedMarker)) {
|
|
2626
|
+
seedDefaultTemplates();
|
|
2627
|
+
try {
|
|
2628
|
+
writeFileSync(seedMarker, "1");
|
|
2629
|
+
} catch {}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
var BUILTIN_PRESETS = [
|
|
2633
|
+
{
|
|
2634
|
+
...getDefaultConfig("wechat-cover-2.35"),
|
|
2635
|
+
id: "tpl_wechat_cover_minimal",
|
|
2636
|
+
name: "微信纯净大图封面 (2.35:1)",
|
|
2637
|
+
slot: "article_cover",
|
|
2638
|
+
algorithm: {
|
|
2639
|
+
...getDefaultConfig("wechat-cover-2.35").algorithm,
|
|
2640
|
+
baseFontSize: 58,
|
|
2641
|
+
scaleRatio: 1.4,
|
|
2642
|
+
verticalAlign: "center",
|
|
2643
|
+
textAlign: "center"
|
|
2644
|
+
},
|
|
2645
|
+
theme: {
|
|
2646
|
+
bgColor: "#09090b",
|
|
2647
|
+
textColor: "#ffffff",
|
|
2648
|
+
headingColor: "#ffffff",
|
|
2649
|
+
accentColor: "#10b981",
|
|
2650
|
+
quoteColor: "#71717a",
|
|
2651
|
+
captionColor: "#71717a",
|
|
2652
|
+
watermarkColor: "#3f3f46",
|
|
2653
|
+
cardBg: "#18181b"
|
|
2654
|
+
},
|
|
2655
|
+
typography: {
|
|
2656
|
+
headingFont: "PingFang SC",
|
|
2657
|
+
bodyFont: "Songti SC"
|
|
2658
|
+
},
|
|
2659
|
+
footerText: "",
|
|
2660
|
+
watermarkText: "",
|
|
2661
|
+
chrome: {
|
|
2662
|
+
showHeader: false,
|
|
2663
|
+
showWatermark: false,
|
|
2664
|
+
showPageNumber: false,
|
|
2665
|
+
showFooter: false,
|
|
2666
|
+
showFooterDivider: false,
|
|
2667
|
+
showFooterText: false,
|
|
2668
|
+
showFillRatio: false
|
|
2669
|
+
},
|
|
2670
|
+
images: [
|
|
2671
|
+
{
|
|
2672
|
+
id: "tpl_img_cover_bg",
|
|
2673
|
+
src: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&auto=format&fit=crop&q=80",
|
|
2674
|
+
alt: "封面装饰图",
|
|
2675
|
+
isAbsolute: true,
|
|
2676
|
+
x: 1350,
|
|
2677
|
+
y: 120,
|
|
2678
|
+
width: 880,
|
|
2679
|
+
height: 760,
|
|
2680
|
+
zIndex: 5,
|
|
2681
|
+
rotate: 0,
|
|
2682
|
+
opacity: 0.95,
|
|
2683
|
+
borderRadius: 24,
|
|
2684
|
+
layoutPreset: "custom"
|
|
2685
|
+
}
|
|
2686
|
+
]
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
...getDefaultConfig("wechat-cover-2.35"),
|
|
2690
|
+
id: "tpl_wechat_cover_editorial",
|
|
2691
|
+
name: "微信杂志标封面 (2.35:1)",
|
|
2692
|
+
slot: "article_cover",
|
|
2693
|
+
algorithm: {
|
|
2694
|
+
...getDefaultConfig("wechat-cover-2.35").algorithm,
|
|
2695
|
+
baseFontSize: 54,
|
|
2696
|
+
scaleRatio: 1.35,
|
|
2697
|
+
verticalAlign: "center",
|
|
2698
|
+
textAlign: "left"
|
|
2699
|
+
},
|
|
2700
|
+
theme: {
|
|
2701
|
+
bgColor: "#ffffff",
|
|
2702
|
+
textColor: "#111827",
|
|
2703
|
+
headingColor: "#000000",
|
|
2704
|
+
accentColor: "#059669",
|
|
2705
|
+
quoteColor: "#4b5563",
|
|
2706
|
+
captionColor: "#6b7280",
|
|
2707
|
+
watermarkColor: "#9ca3af",
|
|
2708
|
+
cardBg: "#f3f4f6"
|
|
2709
|
+
},
|
|
2710
|
+
typography: {
|
|
2711
|
+
headingFont: "PingFang SC",
|
|
2712
|
+
bodyFont: "Songti SC"
|
|
2713
|
+
},
|
|
2714
|
+
footerText: "",
|
|
2715
|
+
watermarkText: "SPECIAL EDITION · 专栏特辑",
|
|
2716
|
+
chrome: {
|
|
2717
|
+
showHeader: true,
|
|
2718
|
+
showWatermark: true,
|
|
2719
|
+
showPageNumber: false,
|
|
2720
|
+
showFooter: false,
|
|
2721
|
+
showFooterDivider: false,
|
|
2722
|
+
showFooterText: false,
|
|
2723
|
+
showFillRatio: false
|
|
2724
|
+
},
|
|
2725
|
+
images: [
|
|
2726
|
+
{
|
|
2727
|
+
id: "tpl_img_editorial_card",
|
|
2728
|
+
src: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=1200&auto=format&fit=crop&q=80",
|
|
2729
|
+
alt: "杂志插图",
|
|
2730
|
+
isAbsolute: true,
|
|
2731
|
+
x: 1400,
|
|
2732
|
+
y: 140,
|
|
2733
|
+
width: 820,
|
|
2734
|
+
height: 720,
|
|
2735
|
+
zIndex: 15,
|
|
2736
|
+
rotate: -2,
|
|
2737
|
+
opacity: 1,
|
|
2738
|
+
borderRadius: 20,
|
|
2739
|
+
layoutPreset: "custom"
|
|
2740
|
+
}
|
|
2741
|
+
]
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
...getDefaultConfig("longform-3-4"),
|
|
2745
|
+
id: "tpl_newspic_poster_cover",
|
|
2746
|
+
name: "贴图封面海报 (3:4)",
|
|
2747
|
+
slot: "newspic_cover",
|
|
2748
|
+
algorithm: {
|
|
2749
|
+
...getDefaultConfig("longform-3-4").algorithm,
|
|
2750
|
+
baseFontSize: 56,
|
|
2751
|
+
scaleRatio: 1.35,
|
|
2752
|
+
verticalAlign: "center",
|
|
2753
|
+
textAlign: "center"
|
|
2754
|
+
},
|
|
2755
|
+
theme: {
|
|
2756
|
+
bgColor: "#121212",
|
|
2757
|
+
textColor: "#ffffff",
|
|
2758
|
+
headingColor: "#ffffff",
|
|
2759
|
+
accentColor: "#f59e0b",
|
|
2760
|
+
quoteColor: "#a3a3a3",
|
|
2761
|
+
captionColor: "#737373",
|
|
2762
|
+
watermarkColor: "#525252",
|
|
2763
|
+
cardBg: "#1c1c1c"
|
|
2764
|
+
},
|
|
2765
|
+
typography: {
|
|
2766
|
+
headingFont: "PingFang SC",
|
|
2767
|
+
bodyFont: "Songti SC"
|
|
2768
|
+
},
|
|
2769
|
+
footerText: "Typeset Studio · 贴图精选",
|
|
2770
|
+
watermarkText: "FEATURED NEWSPIC",
|
|
2771
|
+
chrome: {
|
|
2772
|
+
showHeader: true,
|
|
2773
|
+
showWatermark: true,
|
|
2774
|
+
showPageNumber: false,
|
|
2775
|
+
showFooter: true,
|
|
2776
|
+
showFooterDivider: false,
|
|
2777
|
+
showFooterText: true,
|
|
2778
|
+
showFillRatio: false
|
|
2779
|
+
},
|
|
2780
|
+
images: [
|
|
2781
|
+
{
|
|
2782
|
+
id: "tpl_img_poster_bg",
|
|
2783
|
+
src: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&auto=format&fit=crop&q=80",
|
|
2784
|
+
alt: "海报主图",
|
|
2785
|
+
isAbsolute: true,
|
|
2786
|
+
x: 100,
|
|
2787
|
+
y: 780,
|
|
2788
|
+
width: 1000,
|
|
2789
|
+
height: 650,
|
|
2790
|
+
zIndex: 10,
|
|
2791
|
+
rotate: 0,
|
|
2792
|
+
opacity: 1,
|
|
2793
|
+
borderRadius: 20,
|
|
2794
|
+
layoutPreset: "custom"
|
|
2795
|
+
}
|
|
2796
|
+
]
|
|
2797
|
+
},
|
|
2798
|
+
{
|
|
2799
|
+
...getDefaultConfig("longform-3-4"),
|
|
2800
|
+
id: "tpl_longform_paper_sage",
|
|
2801
|
+
name: "纸白典雅长图文 (3:4)",
|
|
2802
|
+
slot: "newspic_content",
|
|
2803
|
+
theme: {
|
|
2804
|
+
bgColor: "#fbfcfb",
|
|
2805
|
+
textColor: "#1c1917",
|
|
2806
|
+
headingColor: "#0c0a09",
|
|
2807
|
+
accentColor: "#047857",
|
|
2808
|
+
quoteColor: "#44403c",
|
|
2809
|
+
captionColor: "#78716c",
|
|
2810
|
+
watermarkColor: "#a8a29e",
|
|
2811
|
+
cardBg: "#ffffff"
|
|
2812
|
+
},
|
|
2813
|
+
typography: {
|
|
2814
|
+
headingFont: "Songti SC",
|
|
2815
|
+
bodyFont: "Songti SC"
|
|
2816
|
+
},
|
|
2817
|
+
footerText: "Typeset Studio · 纯算法空间排版",
|
|
2818
|
+
watermarkText: "@ANTIGRAVITY",
|
|
2819
|
+
chrome: {
|
|
2820
|
+
showHeader: true,
|
|
2821
|
+
showWatermark: true,
|
|
2822
|
+
showPageNumber: true,
|
|
2823
|
+
showFooter: true,
|
|
2824
|
+
showFooterDivider: true,
|
|
2825
|
+
showFooterText: true,
|
|
2826
|
+
showFillRatio: false
|
|
2827
|
+
}
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
...getDefaultConfig("square-1-1"),
|
|
2831
|
+
id: "tpl_square_quote_card",
|
|
2832
|
+
name: "金句留白方卡 (1:1)",
|
|
2833
|
+
algorithm: {
|
|
2834
|
+
...getDefaultConfig("square-1-1").algorithm,
|
|
2835
|
+
baseFontSize: 50,
|
|
2836
|
+
scaleRatio: 1.3,
|
|
2837
|
+
verticalAlign: "center",
|
|
2838
|
+
textAlign: "center"
|
|
2839
|
+
},
|
|
2840
|
+
theme: {
|
|
2841
|
+
bgColor: "#18181b",
|
|
2842
|
+
textColor: "#fafafa",
|
|
2843
|
+
headingColor: "#ffffff",
|
|
2844
|
+
accentColor: "#f59e0b",
|
|
2845
|
+
quoteColor: "#d4d4d8",
|
|
2846
|
+
captionColor: "#a1a1aa",
|
|
2847
|
+
watermarkColor: "#71717a",
|
|
2848
|
+
cardBg: "#27272a"
|
|
2849
|
+
},
|
|
2850
|
+
typography: {
|
|
2851
|
+
headingFont: "PingFang SC",
|
|
2852
|
+
bodyFont: "Kaiti SC"
|
|
2853
|
+
},
|
|
2854
|
+
footerText: "",
|
|
2855
|
+
watermarkText: "",
|
|
2856
|
+
chrome: {
|
|
2857
|
+
showHeader: false,
|
|
2858
|
+
showWatermark: false,
|
|
2859
|
+
showPageNumber: false,
|
|
2860
|
+
showFooter: false,
|
|
2861
|
+
showFooterDivider: false,
|
|
2862
|
+
showFooterText: false,
|
|
2863
|
+
showFillRatio: false
|
|
2864
|
+
}
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
...getDefaultConfig("wide-16-9"),
|
|
2868
|
+
id: "tpl_wide_tech_banner",
|
|
2869
|
+
name: "极简技术宽屏 (16:9)",
|
|
2870
|
+
algorithm: {
|
|
2871
|
+
...getDefaultConfig("wide-16-9").algorithm,
|
|
2872
|
+
baseFontSize: 46,
|
|
2873
|
+
scaleRatio: 1.3,
|
|
2874
|
+
verticalAlign: "center",
|
|
2875
|
+
textAlign: "left"
|
|
2876
|
+
},
|
|
2877
|
+
theme: {
|
|
2878
|
+
bgColor: "#09090b",
|
|
2879
|
+
textColor: "#f4f4f5",
|
|
2880
|
+
headingColor: "#ffffff",
|
|
2881
|
+
accentColor: "#3b82f6",
|
|
2882
|
+
quoteColor: "#a1a1aa",
|
|
2883
|
+
captionColor: "#71717a",
|
|
2884
|
+
watermarkColor: "#52525b",
|
|
2885
|
+
cardBg: "#18181b"
|
|
2886
|
+
},
|
|
2887
|
+
typography: {
|
|
2888
|
+
headingFont: "PingFang SC",
|
|
2889
|
+
bodyFont: "SourceHanSansSC, sans-serif"
|
|
2890
|
+
},
|
|
2891
|
+
footerText: "TECH ENGINEERING INSIGHTS",
|
|
2892
|
+
watermarkText: "DEV // ARCHITECTURE",
|
|
2893
|
+
chrome: {
|
|
2894
|
+
showHeader: true,
|
|
2895
|
+
showWatermark: true,
|
|
2896
|
+
showPageNumber: false,
|
|
2897
|
+
showFooter: true,
|
|
2898
|
+
showFooterDivider: false,
|
|
2899
|
+
showFooterText: true,
|
|
2900
|
+
showFillRatio: false
|
|
2901
|
+
}
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
...getDefaultConfig("longform-3-4"),
|
|
2905
|
+
id: "tpl_longform_vintage_news",
|
|
2906
|
+
name: "复古报刊多图 (3:4)",
|
|
2907
|
+
slot: "newspic_content",
|
|
2908
|
+
algorithm: {
|
|
2909
|
+
...getDefaultConfig("longform-3-4").algorithm,
|
|
2910
|
+
baseFontSize: 38,
|
|
2911
|
+
scaleRatio: 1.25,
|
|
2912
|
+
verticalAlign: "top",
|
|
2913
|
+
textAlign: "left"
|
|
2914
|
+
},
|
|
2915
|
+
theme: {
|
|
2916
|
+
bgColor: "#f7f4ee",
|
|
2917
|
+
textColor: "#262320",
|
|
2918
|
+
headingColor: "#141210",
|
|
2919
|
+
accentColor: "#991b1b",
|
|
2920
|
+
quoteColor: "#57534e",
|
|
2921
|
+
captionColor: "#78716c",
|
|
2922
|
+
watermarkColor: "#a8a29e",
|
|
2923
|
+
cardBg: "#eee9df"
|
|
2924
|
+
},
|
|
2925
|
+
typography: {
|
|
2926
|
+
headingFont: "SourceHanSerifSC, serif",
|
|
2927
|
+
bodyFont: "SourceHanSerifSC, serif"
|
|
2928
|
+
},
|
|
2929
|
+
footerText: "WEEKLY REPORT · 观察与沉淀",
|
|
2930
|
+
watermarkText: "VOL. 42 // THE CHRONICLE",
|
|
2931
|
+
chrome: {
|
|
2932
|
+
showHeader: true,
|
|
2933
|
+
showWatermark: true,
|
|
2934
|
+
showPageNumber: true,
|
|
2935
|
+
showFooter: true,
|
|
2936
|
+
showFooterDivider: true,
|
|
2937
|
+
showFooterText: true,
|
|
2938
|
+
showFillRatio: false
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
];
|
|
2942
|
+
function seedDefaultTemplates() {
|
|
2943
|
+
if (!existsSync(TEMPLATES_DIR))
|
|
2944
|
+
return;
|
|
2945
|
+
for (const preset of BUILTIN_PRESETS) {
|
|
2946
|
+
const filePath = join(TEMPLATES_DIR, `${preset.id}.json`);
|
|
2947
|
+
if (!existsSync(filePath)) {
|
|
2948
|
+
try {
|
|
2949
|
+
writeFileSync(filePath, JSON.stringify(preset, null, 2), "utf-8");
|
|
2950
|
+
} catch {}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
var DEFAULT_SETTINGS_PATH = join(resolveDataDir(), "default-template.json");
|
|
2955
|
+
var BUILTIN_SLOT_DEFAULTS = {
|
|
2956
|
+
article_cover: "tpl_wechat_cover_minimal",
|
|
2957
|
+
newspic_cover: "tpl_newspic_poster_cover",
|
|
2958
|
+
newspic_content: "tpl_longform_paper_sage"
|
|
2959
|
+
};
|
|
2960
|
+
function getSlotDefaultTemplateId(slot, account = "default") {
|
|
2961
|
+
if (existsSync(DEFAULT_SETTINGS_PATH)) {
|
|
2962
|
+
try {
|
|
2963
|
+
const data = JSON.parse(readFileSync(DEFAULT_SETTINGS_PATH, "utf-8"));
|
|
2964
|
+
if (account && data.accounts?.[account]?.[slot]) {
|
|
2965
|
+
return data.accounts[account][slot];
|
|
2966
|
+
}
|
|
2967
|
+
if (data.slots?.[slot]) {
|
|
2968
|
+
return data.slots[slot];
|
|
2969
|
+
}
|
|
2970
|
+
} catch {}
|
|
2971
|
+
}
|
|
2972
|
+
return BUILTIN_SLOT_DEFAULTS[slot] || "tpl_wechat_cover_minimal";
|
|
2973
|
+
}
|
|
2974
|
+
function setSlotDefaultTemplateId(slot, templateId, account = "default") {
|
|
2975
|
+
const dir = dirname(DEFAULT_SETTINGS_PATH);
|
|
2976
|
+
if (!existsSync(dir)) {
|
|
2977
|
+
mkdirSync(dir, { recursive: true });
|
|
2978
|
+
}
|
|
2979
|
+
let current = { slots: { ...BUILTIN_SLOT_DEFAULTS }, accounts: {} };
|
|
2980
|
+
if (existsSync(DEFAULT_SETTINGS_PATH)) {
|
|
2981
|
+
try {
|
|
2982
|
+
current = JSON.parse(readFileSync(DEFAULT_SETTINGS_PATH, "utf-8"));
|
|
2983
|
+
} catch {}
|
|
2984
|
+
}
|
|
2985
|
+
if (!current.slots)
|
|
2986
|
+
current.slots = { ...BUILTIN_SLOT_DEFAULTS };
|
|
2987
|
+
if (!current.accounts)
|
|
2988
|
+
current.accounts = {};
|
|
2989
|
+
if (!account || account === "default") {
|
|
2990
|
+
current.slots[slot] = templateId;
|
|
2991
|
+
} else {
|
|
2992
|
+
if (!current.accounts[account])
|
|
2993
|
+
current.accounts[account] = {};
|
|
2994
|
+
current.accounts[account][slot] = templateId;
|
|
2995
|
+
}
|
|
2996
|
+
writeFileSync(DEFAULT_SETTINGS_PATH, JSON.stringify(current, null, 2), "utf-8");
|
|
2997
|
+
}
|
|
2998
|
+
function getDefaultTemplateId() {
|
|
2999
|
+
if (existsSync(DEFAULT_SETTINGS_PATH)) {
|
|
3000
|
+
try {
|
|
3001
|
+
const data = JSON.parse(readFileSync(DEFAULT_SETTINGS_PATH, "utf-8"));
|
|
3002
|
+
return data.defaultTemplateId || data.slots?.article_cover || null;
|
|
3003
|
+
} catch {
|
|
3004
|
+
return null;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return BUILTIN_SLOT_DEFAULTS.article_cover;
|
|
3008
|
+
}
|
|
3009
|
+
function setDefaultTemplateId(id) {
|
|
3010
|
+
const dir = dirname(DEFAULT_SETTINGS_PATH);
|
|
3011
|
+
if (!existsSync(dir)) {
|
|
3012
|
+
mkdirSync(dir, { recursive: true });
|
|
3013
|
+
}
|
|
3014
|
+
let current = { slots: { ...BUILTIN_SLOT_DEFAULTS }, accounts: {} };
|
|
3015
|
+
if (existsSync(DEFAULT_SETTINGS_PATH)) {
|
|
3016
|
+
try {
|
|
3017
|
+
current = JSON.parse(readFileSync(DEFAULT_SETTINGS_PATH, "utf-8"));
|
|
3018
|
+
} catch {}
|
|
3019
|
+
}
|
|
3020
|
+
current.defaultTemplateId = id;
|
|
3021
|
+
writeFileSync(DEFAULT_SETTINGS_PATH, JSON.stringify(current, null, 2), "utf-8");
|
|
3022
|
+
}
|
|
3023
|
+
function findTemplateByKeyOrName(key) {
|
|
3024
|
+
const lowerKey = key.trim().toLowerCase();
|
|
3025
|
+
if (!lowerKey)
|
|
3026
|
+
return null;
|
|
3027
|
+
ensureTemplatesDir();
|
|
3028
|
+
if (!existsSync(TEMPLATES_DIR))
|
|
3029
|
+
return BUILTIN_PRESETS.find((p) => p.id?.toLowerCase() === lowerKey || p.name.toLowerCase() === lowerKey) ?? null;
|
|
3030
|
+
const directPath = templatePath(key);
|
|
3031
|
+
if (directPath && existsSync(directPath)) {
|
|
3032
|
+
try {
|
|
3033
|
+
return JSON.parse(readFileSync(directPath, "utf-8"));
|
|
3034
|
+
} catch {}
|
|
3035
|
+
}
|
|
3036
|
+
const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".json"));
|
|
3037
|
+
const templates = [];
|
|
3038
|
+
for (const file of files) {
|
|
3039
|
+
try {
|
|
3040
|
+
const content = readFileSync(join(TEMPLATES_DIR, file), "utf-8");
|
|
3041
|
+
const tpl = JSON.parse(content);
|
|
3042
|
+
if (tpl.id?.toLowerCase() === lowerKey || tpl.name.toLowerCase() === lowerKey) {
|
|
3043
|
+
return tpl;
|
|
3044
|
+
}
|
|
3045
|
+
templates.push(tpl);
|
|
3046
|
+
} catch {}
|
|
3047
|
+
}
|
|
3048
|
+
return templates.find((tpl) => tpl.name.toLowerCase().includes(lowerKey)) ?? null;
|
|
3049
|
+
}
|
|
3050
|
+
function templatePath(id) {
|
|
3051
|
+
return /^[a-zA-Z0-9_-]+$/.test(id) ? join(TEMPLATES_DIR, `${id}.json`) : null;
|
|
3052
|
+
}
|
|
3053
|
+
var templatesRouter = new Hono3;
|
|
3054
|
+
templatesRouter.get("/", (c) => {
|
|
3055
|
+
ensureTemplatesDir();
|
|
3056
|
+
const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".json"));
|
|
3057
|
+
const templates = [];
|
|
3058
|
+
for (const file of files) {
|
|
3059
|
+
try {
|
|
3060
|
+
const content = readFileSync(join(TEMPLATES_DIR, file), "utf-8");
|
|
3061
|
+
const parsed = JSON.parse(content);
|
|
3062
|
+
templates.push(parsed);
|
|
3063
|
+
} catch {}
|
|
3064
|
+
}
|
|
3065
|
+
return c.json({ ok: true, templates });
|
|
3066
|
+
});
|
|
3067
|
+
templatesRouter.get("/slots", (c) => {
|
|
3068
|
+
ensureTemplatesDir();
|
|
3069
|
+
const account = c.req.query("account") || "default";
|
|
3070
|
+
const articleCoverId = getSlotDefaultTemplateId("article_cover", account);
|
|
3071
|
+
const newspicCoverId = getSlotDefaultTemplateId("newspic_cover", account);
|
|
3072
|
+
const newspicContentId = getSlotDefaultTemplateId("newspic_content", account);
|
|
3073
|
+
return c.json({
|
|
3074
|
+
ok: true,
|
|
3075
|
+
account,
|
|
3076
|
+
slots: {
|
|
3077
|
+
article_cover: {
|
|
3078
|
+
id: articleCoverId,
|
|
3079
|
+
template: findTemplateByKeyOrName(articleCoverId)
|
|
3080
|
+
},
|
|
3081
|
+
newspic_cover: {
|
|
3082
|
+
id: newspicCoverId,
|
|
3083
|
+
template: findTemplateByKeyOrName(newspicCoverId)
|
|
3084
|
+
},
|
|
3085
|
+
newspic_content: {
|
|
3086
|
+
id: newspicContentId,
|
|
3087
|
+
template: findTemplateByKeyOrName(newspicContentId)
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
});
|
|
3092
|
+
templatesRouter.post("/slots", async (c) => {
|
|
3093
|
+
ensureTemplatesDir();
|
|
3094
|
+
const body = await c.req.json();
|
|
3095
|
+
if (!body.slot || !body.templateId) {
|
|
3096
|
+
return c.json({ ok: false, error: "slot and templateId are required" }, 400);
|
|
3097
|
+
}
|
|
3098
|
+
const tpl = findTemplateByKeyOrName(body.templateId);
|
|
3099
|
+
if (!tpl) {
|
|
3100
|
+
return c.json({ ok: false, error: `Template '${body.templateId}' not found` }, 404);
|
|
3101
|
+
}
|
|
3102
|
+
const account = body.account || "default";
|
|
3103
|
+
setSlotDefaultTemplateId(body.slot, tpl.id || body.templateId, account);
|
|
3104
|
+
return c.json({
|
|
3105
|
+
ok: true,
|
|
3106
|
+
slot: body.slot,
|
|
3107
|
+
account,
|
|
3108
|
+
templateId: tpl.id || body.templateId,
|
|
3109
|
+
template: tpl
|
|
3110
|
+
});
|
|
3111
|
+
});
|
|
3112
|
+
templatesRouter.get("/default", (c) => {
|
|
3113
|
+
ensureTemplatesDir();
|
|
3114
|
+
const defaultId = getDefaultTemplateId();
|
|
3115
|
+
let template = null;
|
|
3116
|
+
if (defaultId) {
|
|
3117
|
+
template = findTemplateByKeyOrName(defaultId);
|
|
3118
|
+
}
|
|
3119
|
+
return c.json({ ok: true, defaultTemplateId: defaultId, template });
|
|
3120
|
+
});
|
|
3121
|
+
templatesRouter.post("/default", async (c) => {
|
|
3122
|
+
ensureTemplatesDir();
|
|
3123
|
+
const body = await c.req.json();
|
|
3124
|
+
const id = body.templateId || body.templateKey;
|
|
3125
|
+
if (!id) {
|
|
3126
|
+
return c.json({ ok: false, error: "templateId is required" }, 400);
|
|
3127
|
+
}
|
|
3128
|
+
const tpl = findTemplateByKeyOrName(id);
|
|
3129
|
+
if (!tpl) {
|
|
3130
|
+
return c.json({ ok: false, error: "Template not found" }, 404);
|
|
3131
|
+
}
|
|
3132
|
+
setDefaultTemplateId(tpl.id || id);
|
|
3133
|
+
return c.json({ ok: true, defaultTemplateId: tpl.id || id, template: tpl });
|
|
3134
|
+
});
|
|
3135
|
+
templatesRouter.get("/resolve/:key", (c) => {
|
|
3136
|
+
ensureTemplatesDir();
|
|
3137
|
+
const key = c.req.param("key");
|
|
3138
|
+
const tpl = findTemplateByKeyOrName(key);
|
|
3139
|
+
if (!tpl) {
|
|
3140
|
+
return c.json({ ok: false, error: `Template matching '${key}' not found` }, 404);
|
|
3141
|
+
}
|
|
3142
|
+
return c.json({ ok: true, template: tpl });
|
|
3143
|
+
});
|
|
3144
|
+
templatesRouter.get("/:id", (c) => {
|
|
3145
|
+
ensureTemplatesDir();
|
|
3146
|
+
const id = c.req.param("id");
|
|
3147
|
+
const filePath = templatePath(id);
|
|
3148
|
+
if (!filePath)
|
|
3149
|
+
return c.json({ ok: false, error: "Invalid template id" }, 400);
|
|
3150
|
+
if (!existsSync(filePath)) {
|
|
3151
|
+
return c.json({ ok: false, error: "Template not found" }, 404);
|
|
3152
|
+
}
|
|
3153
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3154
|
+
return c.json({ ok: true, template: JSON.parse(content) });
|
|
3155
|
+
});
|
|
3156
|
+
templatesRouter.post("/", async (c) => {
|
|
3157
|
+
ensureTemplatesDir();
|
|
3158
|
+
const body = await c.req.json();
|
|
3159
|
+
if (!body.name) {
|
|
3160
|
+
return c.json({ ok: false, error: "Template name is required" }, 400);
|
|
3161
|
+
}
|
|
3162
|
+
try {
|
|
3163
|
+
validateTemplateAssets(body.images);
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : "无效的模板素材" }, 400);
|
|
3166
|
+
}
|
|
3167
|
+
const id = body.id || `tpl_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
3168
|
+
const templateToSave = {
|
|
3169
|
+
...body,
|
|
3170
|
+
id
|
|
3171
|
+
};
|
|
3172
|
+
const filePath = templatePath(id);
|
|
3173
|
+
if (!filePath)
|
|
3174
|
+
return c.json({ ok: false, error: "Invalid template id" }, 400);
|
|
3175
|
+
writeFileSync(filePath, JSON.stringify(templateToSave, null, 2), "utf-8");
|
|
3176
|
+
return c.json({ ok: true, template: templateToSave });
|
|
3177
|
+
});
|
|
3178
|
+
templatesRouter.delete("/:id", (c) => {
|
|
3179
|
+
ensureTemplatesDir();
|
|
3180
|
+
const id = c.req.param("id");
|
|
3181
|
+
const filePath = templatePath(id);
|
|
3182
|
+
if (!filePath)
|
|
3183
|
+
return c.json({ ok: false, error: "Invalid template id" }, 400);
|
|
3184
|
+
if (existsSync(filePath)) {
|
|
3185
|
+
unlinkSync(filePath);
|
|
3186
|
+
return c.json({ ok: true, message: "Template deleted" });
|
|
3187
|
+
}
|
|
3188
|
+
return c.json({ ok: false, error: "Template not found" }, 404);
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3191
|
+
// src/server/routes/render.ts
|
|
3192
|
+
var renderRouter = new Hono3;
|
|
3193
|
+
renderRouter.onError((error, c) => {
|
|
3194
|
+
if (error.message.startsWith("模板素材 "))
|
|
3195
|
+
return c.json({ ok: false, error: error.message }, 422);
|
|
3196
|
+
if (error instanceof RangeError)
|
|
3197
|
+
return c.json({ ok: false, error: error.message }, 400);
|
|
3198
|
+
console.error("渲染失败", error);
|
|
3199
|
+
return c.json({ ok: false, error: "Render failed" }, 500);
|
|
3200
|
+
});
|
|
3201
|
+
function inferSlot(route, kind, aspectRatio) {
|
|
3202
|
+
if (route === "wechat-article" || kind === "article_cover" || aspectRatio === "wechat-cover-2.35") {
|
|
3203
|
+
return "article_cover";
|
|
3204
|
+
}
|
|
3205
|
+
if (route === "wechat-newspic") {
|
|
3206
|
+
if (kind === "content" || kind === "pages") {
|
|
3207
|
+
return "newspic_content";
|
|
3208
|
+
}
|
|
3209
|
+
return "newspic_cover";
|
|
3210
|
+
}
|
|
3211
|
+
if (kind === "cover") {
|
|
3212
|
+
return "article_cover";
|
|
3213
|
+
}
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
function resolveTypesetResult(body) {
|
|
3217
|
+
if (body.result) {
|
|
3218
|
+
return {
|
|
3219
|
+
templateId: body.result.config?.id,
|
|
3220
|
+
templateName: body.result.config?.name,
|
|
3221
|
+
slot: body.result.config?.slot,
|
|
3222
|
+
result: body.result
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
let markdown = body.markdown || body.text;
|
|
3226
|
+
if (!markdown && body.title) {
|
|
3227
|
+
markdown = `# ${body.title}`;
|
|
3228
|
+
}
|
|
3229
|
+
if (!markdown) {
|
|
3230
|
+
markdown = `# 示例标题
|
|
3231
|
+
正文内容`;
|
|
3232
|
+
}
|
|
3233
|
+
const targetKey = body.templateId || body.templateKey || body.template;
|
|
3234
|
+
let baseConfig = null;
|
|
3235
|
+
if (targetKey) {
|
|
3236
|
+
baseConfig = findTemplateByKeyOrName(targetKey);
|
|
3237
|
+
if (!baseConfig)
|
|
3238
|
+
throw new RangeError(`Template '${targetKey}' not found`);
|
|
3239
|
+
}
|
|
3240
|
+
const resolvedSlot = body.slot || inferSlot(body.route, body.kind, body.config?.aspectRatio);
|
|
3241
|
+
if (!baseConfig && resolvedSlot) {
|
|
3242
|
+
const slotDefaultId = getSlotDefaultTemplateId(resolvedSlot, body.account || "default");
|
|
3243
|
+
if (slotDefaultId) {
|
|
3244
|
+
baseConfig = findTemplateByKeyOrName(slotDefaultId);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
if (!baseConfig) {
|
|
3248
|
+
const defaultId = getDefaultTemplateId();
|
|
3249
|
+
if (defaultId) {
|
|
3250
|
+
baseConfig = findTemplateByKeyOrName(defaultId);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (!baseConfig) {
|
|
3254
|
+
const isCover = resolvedSlot === "article_cover" || body.route === "wechat-article";
|
|
3255
|
+
baseConfig = getDefaultConfig(isCover ? "wechat-cover-2.35" : "longform-3-4");
|
|
3256
|
+
}
|
|
3257
|
+
const overrides = body.preserveTemplateStyle && baseConfig.id ? {} : body.config;
|
|
3258
|
+
const effectiveConfig = {
|
|
3259
|
+
...baseConfig,
|
|
3260
|
+
...overrides || {},
|
|
3261
|
+
algorithm: {
|
|
3262
|
+
...baseConfig.algorithm,
|
|
3263
|
+
...overrides?.algorithm || {}
|
|
3264
|
+
},
|
|
3265
|
+
theme: {
|
|
3266
|
+
...baseConfig.theme,
|
|
3267
|
+
...overrides?.theme || {}
|
|
3268
|
+
},
|
|
3269
|
+
typography: {
|
|
3270
|
+
...baseConfig.typography,
|
|
3271
|
+
...overrides?.typography || {}
|
|
3272
|
+
},
|
|
3273
|
+
chrome: { ...baseConfig.chrome, ...overrides?.chrome }
|
|
3274
|
+
};
|
|
3275
|
+
const resolvedImages = applyTemplateImages(body.images || [], effectiveConfig.images, body.templateImageOverrides);
|
|
3276
|
+
let result;
|
|
3277
|
+
while (true) {
|
|
3278
|
+
try {
|
|
3279
|
+
result = runTypeset({
|
|
3280
|
+
markdown,
|
|
3281
|
+
images: resolvedImages,
|
|
3282
|
+
config: effectiveConfig,
|
|
3283
|
+
highlightWords: body.highlightWords || [],
|
|
3284
|
+
minPages: body.minPages,
|
|
3285
|
+
maxPages: body.maxPages,
|
|
3286
|
+
pageFillRatios: body.pageFillRatios
|
|
3287
|
+
});
|
|
3288
|
+
break;
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
const theme = effectiveConfig.coverTheme;
|
|
3291
|
+
if (!theme || body.kind !== "cover" || !(error instanceof Error) || !/exceeds maxPages|cannot fit/.test(error.message))
|
|
3292
|
+
throw error;
|
|
3293
|
+
const title = theme.typography.title;
|
|
3294
|
+
if (!Number.isFinite(title.fontSize) || !Number.isFinite(title.minFontSize) || title.minFontSize <= 0) {
|
|
3295
|
+
throw new RangeError("Invalid cover theme font size");
|
|
3296
|
+
}
|
|
3297
|
+
if (title.fontSize <= title.minFontSize)
|
|
3298
|
+
throw error;
|
|
3299
|
+
effectiveConfig.coverTheme = {
|
|
3300
|
+
...theme,
|
|
3301
|
+
typography: { ...theme.typography, title: {
|
|
3302
|
+
...title,
|
|
3303
|
+
fontSize: Math.max(title.minFontSize, Math.floor(title.fontSize * 0.9))
|
|
3304
|
+
} }
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return {
|
|
3309
|
+
templateId: baseConfig.id,
|
|
3310
|
+
templateName: baseConfig.name,
|
|
3311
|
+
slot: resolvedSlot,
|
|
3312
|
+
result
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
renderRouter.post("/", async (c) => {
|
|
3316
|
+
const body = await c.req.json();
|
|
3317
|
+
const resolved = resolveTypesetResult(body);
|
|
3318
|
+
return c.json({
|
|
3319
|
+
ok: true,
|
|
3320
|
+
templateId: resolved.templateId,
|
|
3321
|
+
templateName: resolved.templateName,
|
|
3322
|
+
slot: resolved.slot,
|
|
3323
|
+
result: resolved.result
|
|
3324
|
+
});
|
|
3325
|
+
});
|
|
3326
|
+
renderRouter.post("/png", async (c) => {
|
|
3327
|
+
const body = await c.req.json();
|
|
3328
|
+
const { result } = resolveTypesetResult(body);
|
|
3329
|
+
const targetPageNumber = body.pageNumber ?? 1;
|
|
3330
|
+
const targetPage = result.pages.find((p) => p.pageNumber === targetPageNumber) || result.pages[0];
|
|
3331
|
+
if (!targetPage) {
|
|
3332
|
+
return c.json({ ok: false, error: "No page available to render" }, 404);
|
|
3333
|
+
}
|
|
3334
|
+
const pngBuffer = await renderPageToPng(targetPage, result.canvasSpec, result.config, result.pageCount);
|
|
3335
|
+
return new Response(new Uint8Array(pngBuffer), {
|
|
3336
|
+
status: 200,
|
|
3337
|
+
headers: {
|
|
3338
|
+
"Content-Type": "image/png",
|
|
3339
|
+
"Content-Length": String(pngBuffer.length),
|
|
3340
|
+
"Content-Disposition": `inline; filename="typeset-${result.canvasSpec.id}-p${targetPage.pageNumber}.png"`,
|
|
3341
|
+
"Cache-Control": "no-cache"
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
});
|
|
3345
|
+
renderRouter.post("/export", async (c) => {
|
|
3346
|
+
const body = await c.req.json();
|
|
3347
|
+
const { result } = resolveTypesetResult(body);
|
|
3348
|
+
const pngOutputs = await renderTypesetResultToPngs(result);
|
|
3349
|
+
return c.json({
|
|
3350
|
+
ok: true,
|
|
3351
|
+
pageCount: pngOutputs.length,
|
|
3352
|
+
pages: pngOutputs.map((item) => ({
|
|
3353
|
+
pageNumber: item.pageNumber,
|
|
3354
|
+
dataUrl: `data:image/png;base64,${item.buffer.toString("base64")}`,
|
|
3355
|
+
byteLength: item.buffer.length,
|
|
3356
|
+
imageIds: result.pages.find((page) => page.pageNumber === item.pageNumber).images.map((image) => image.id),
|
|
3357
|
+
bodyImageCount: result.pages.find((page) => page.pageNumber === item.pageNumber).images.filter((image) => !image.templateAssetKey).length,
|
|
3358
|
+
bodyImageSources: result.pages.find((page) => page.pageNumber === item.pageNumber).images.filter((image) => !image.templateAssetKey).map((image) => image.src),
|
|
3359
|
+
bodyImageIds: result.pages.find((page) => page.pageNumber === item.pageNumber).images.filter((image) => !image.templateAssetKey).map((image) => image.id),
|
|
3360
|
+
imageCount: result.pages.find((page) => page.pageNumber === item.pageNumber).images.length,
|
|
3361
|
+
imageSources: result.pages.find((page) => page.pageNumber === item.pageNumber).images.map((image) => image.src)
|
|
3362
|
+
}))
|
|
3363
|
+
});
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
export { validateTemplateAssets, applyTemplateImages, getDefaultConfig, runTypeset, renderRouter, resolveTypesetResult };
|