slackmark-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/create-node-converter.d.ts +72 -0
- package/dist/create-node-converter.d.ts.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/image-output.d.ts +9 -0
- package/dist/image-output.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1792 -0
- package/dist/index.js.map +1 -0
- package/dist/katex-assets.d.ts +19 -0
- package/dist/katex-assets.d.ts.map +1 -0
- package/dist/katex-renderer.d.ts +39 -0
- package/dist/katex-renderer.d.ts.map +1 -0
- package/dist/mermaid-assets.d.ts +9 -0
- package/dist/mermaid-assets.d.ts.map +1 -0
- package/dist/mermaid-renderer.d.ts +33 -0
- package/dist/mermaid-renderer.d.ts.map +1 -0
- package/dist/node-options.d.ts +6 -0
- package/dist/node-options.d.ts.map +1 -0
- package/dist/operation-control.d.ts +18 -0
- package/dist/operation-control.d.ts.map +1 -0
- package/dist/puppeteer-render-runtime.d.ts +76 -0
- package/dist/puppeteer-render-runtime.d.ts.map +1 -0
- package/dist/puppeteer.d.ts +7 -0
- package/dist/puppeteer.d.ts.map +1 -0
- package/dist/puppeteer.js +723 -0
- package/dist/puppeteer.js.map +1 -0
- package/dist/renderers.d.ts +5 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +551 -0
- package/dist/renderers.js.map +1 -0
- package/dist/status.d.ts +20 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/upload-scheduler.d.ts +32 -0
- package/dist/upload-scheduler.d.ts.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1792 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { ConfigurationError, ConfigurationError as ConfigurationError$1, OperationAbortedError, OperationAbortedError as OperationAbortedError$1, OperationDeadlineExceededError, OperationDeadlineExceededError as OperationDeadlineExceededError$1, OperationError, OperationErrorCode, OperationErrorCode as OperationErrorCode$1, OperationShutdownError, OperationShutdownError as OperationShutdownError$1, RecordingReceiptCollector, attachReceipts, cloneOperationError, createConverter, isConfigurationError, isOperationError, lockReceiptSnapshot, resolveCapabilities } from "slackmark";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { SlackUploadError, SlackUploadStage } from "slackmark/slack";
|
|
7
|
+
//#region src/errors.ts
|
|
8
|
+
const NodeRenderErrorCode = {
|
|
9
|
+
AssetMissing: "asset_missing",
|
|
10
|
+
BrowserBinaryMissing: "browser_binary_missing",
|
|
11
|
+
BrowserDisconnected: "browser_disconnected",
|
|
12
|
+
BrowserFailed: "browser_failed",
|
|
13
|
+
CleanupFailed: "cleanup_failed",
|
|
14
|
+
CloseFailed: "close_failed",
|
|
15
|
+
ContextFailed: "context_failed",
|
|
16
|
+
DimensionLimit: "dimension_limit",
|
|
17
|
+
OutputLimit: "output_limit",
|
|
18
|
+
QueueFull: "queue_full",
|
|
19
|
+
RenderFailed: "render_failed",
|
|
20
|
+
SourceLimit: "source_limit"
|
|
21
|
+
};
|
|
22
|
+
const NodeRenderStage = {
|
|
23
|
+
Admission: "admission",
|
|
24
|
+
AssetLoad: "asset_load",
|
|
25
|
+
Browser: "browser",
|
|
26
|
+
BrowserCleanup: "browser_cleanup",
|
|
27
|
+
BrowserClose: "browser_close",
|
|
28
|
+
BrowserLaunch: "browser_launch",
|
|
29
|
+
ContextCleanup: "context_cleanup",
|
|
30
|
+
ContextCreate: "context_create",
|
|
31
|
+
Layout: "layout",
|
|
32
|
+
OutputValidation: "output_validation",
|
|
33
|
+
Render: "render",
|
|
34
|
+
SourceValidation: "source_validation"
|
|
35
|
+
};
|
|
36
|
+
var NodeShutdownTimeoutError = class extends Error {
|
|
37
|
+
name;
|
|
38
|
+
code;
|
|
39
|
+
stage;
|
|
40
|
+
retryable;
|
|
41
|
+
status;
|
|
42
|
+
receipts;
|
|
43
|
+
settledReceipts;
|
|
44
|
+
constructor(status, receipts, settledReceipts) {
|
|
45
|
+
super("Node converter shutdown timed out");
|
|
46
|
+
this.name = "NodeShutdownTimeoutError";
|
|
47
|
+
this.code = "shutdown_timeout";
|
|
48
|
+
this.stage = "close";
|
|
49
|
+
this.retryable = true;
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.receipts = Object.freeze([...receipts]);
|
|
52
|
+
this.settledReceipts = settledReceipts;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Sanitized failure from the Node rendering boundary.
|
|
57
|
+
*/
|
|
58
|
+
var NodeRenderError = class extends Error {
|
|
59
|
+
code;
|
|
60
|
+
stage;
|
|
61
|
+
retryable;
|
|
62
|
+
rendererId;
|
|
63
|
+
constructor(code, message, options = {
|
|
64
|
+
stage: "render",
|
|
65
|
+
retryable: false
|
|
66
|
+
}) {
|
|
67
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
68
|
+
this.name = "NodeRenderError";
|
|
69
|
+
this.code = code;
|
|
70
|
+
this.stage = options.stage;
|
|
71
|
+
this.retryable = options.retryable;
|
|
72
|
+
this.rendererId = options.rendererId;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function normalizeNodeRenderError(error, code, message, stage = NodeRenderStage.Render, retryable = false) {
|
|
76
|
+
return error instanceof NodeRenderError || isOperationError(error) ? error : new NodeRenderError(code, message, {
|
|
77
|
+
stage,
|
|
78
|
+
retryable,
|
|
79
|
+
cause: error
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/katex-assets.ts
|
|
84
|
+
const resolver$1 = createRequire(import.meta.url);
|
|
85
|
+
const WOFF2_URL_RE = /url\(fonts\/([A-Za-z0-9_-]+\.woff2)\)/g;
|
|
86
|
+
const LEGACY_FONT_SOURCE_RE = /, url\(fonts\/[A-Za-z0-9_-]+\.(?:woff|ttf)\) format\("(?:woff|truetype)"\)/g;
|
|
87
|
+
/**
|
|
88
|
+
* Reads KaTeX's installed CSS/fonts and embeds WOFF2 files as data URLs once.
|
|
89
|
+
*/
|
|
90
|
+
var LocalKatexAssetLoader = class {
|
|
91
|
+
cssPromise;
|
|
92
|
+
scriptPromise;
|
|
93
|
+
constructor() {
|
|
94
|
+
this.cssPromise = void 0;
|
|
95
|
+
this.scriptPromise = void 0;
|
|
96
|
+
}
|
|
97
|
+
loadScript() {
|
|
98
|
+
this.scriptPromise ??= readFile(resolver$1.resolve("katex/dist/katex.min.js"), "utf8").catch((error) => {
|
|
99
|
+
this.scriptPromise = void 0;
|
|
100
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.AssetMissing, "Unable to load local KaTeX script", "asset_load", false);
|
|
101
|
+
});
|
|
102
|
+
return this.scriptPromise;
|
|
103
|
+
}
|
|
104
|
+
loadCss() {
|
|
105
|
+
this.cssPromise ??= this.loadCssFromPackage().catch((error) => {
|
|
106
|
+
this.cssPromise = void 0;
|
|
107
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.AssetMissing, "Unable to load local KaTeX assets", "asset_load", false);
|
|
108
|
+
});
|
|
109
|
+
return this.cssPromise;
|
|
110
|
+
}
|
|
111
|
+
async loadCssFromPackage() {
|
|
112
|
+
const cssPath = resolver$1.resolve("katex/dist/katex.css");
|
|
113
|
+
const css = await readFile(cssPath, "utf8");
|
|
114
|
+
const fontNames = /* @__PURE__ */ new Set();
|
|
115
|
+
for (const match of css.matchAll(WOFF2_URL_RE)) {
|
|
116
|
+
const fontName = match[1];
|
|
117
|
+
if (fontName !== void 0) fontNames.add(fontName);
|
|
118
|
+
}
|
|
119
|
+
const encodedFonts = await Promise.all([...fontNames].map(async (fontName) => {
|
|
120
|
+
return [fontName, (await readFile(join(dirname(cssPath), "fonts", fontName))).toString("base64")];
|
|
121
|
+
}));
|
|
122
|
+
let embedded = css.replace(LEGACY_FONT_SOURCE_RE, "");
|
|
123
|
+
for (const [fontName, base64] of encodedFonts) embedded = embedded.replaceAll(`url(fonts/${fontName})`, `url(data:font/woff2;base64,${base64})`);
|
|
124
|
+
return embedded;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/image-output.ts
|
|
129
|
+
const PNG_SIGNATURE = new Uint8Array([
|
|
130
|
+
137,
|
|
131
|
+
80,
|
|
132
|
+
78,
|
|
133
|
+
71,
|
|
134
|
+
13,
|
|
135
|
+
10,
|
|
136
|
+
26,
|
|
137
|
+
10
|
|
138
|
+
]);
|
|
139
|
+
const PNG_WIDTH_OFFSET = 16;
|
|
140
|
+
const PNG_HEIGHT_OFFSET = 20;
|
|
141
|
+
const PNG_HEADER_BYTES = 24;
|
|
142
|
+
const PNG_IHDR = new Uint8Array([
|
|
143
|
+
73,
|
|
144
|
+
72,
|
|
145
|
+
68,
|
|
146
|
+
82
|
|
147
|
+
]);
|
|
148
|
+
const PNG_IHDR_OFFSET = 12;
|
|
149
|
+
function deterministicPngFilename(prefix, source, optionsFingerprint) {
|
|
150
|
+
return `${prefix}-${createHash("sha256").update(prefix).update("\0").update(optionsFingerprint).update("\0").update(source).digest("hex").slice(0, 16)}.png`;
|
|
151
|
+
}
|
|
152
|
+
function assertSourceLimit(source, maxSourceChars) {
|
|
153
|
+
if (source.length > maxSourceChars) throw new NodeRenderError(NodeRenderErrorCode.SourceLimit, `Render source exceeds ${String(maxSourceChars)} characters`, {
|
|
154
|
+
stage: NodeRenderStage.SourceValidation,
|
|
155
|
+
retryable: false
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function assertPngLimits(data, limits) {
|
|
159
|
+
if (data.byteLength > limits.maxOutputBytes) throw new NodeRenderError(NodeRenderErrorCode.OutputLimit, `Rendered image exceeds ${String(limits.maxOutputBytes)} bytes`, {
|
|
160
|
+
stage: NodeRenderStage.OutputValidation,
|
|
161
|
+
retryable: false
|
|
162
|
+
});
|
|
163
|
+
const dimensions = readPngDimensions(data);
|
|
164
|
+
if (dimensions === void 0) throw new NodeRenderError(NodeRenderErrorCode.RenderFailed, "Renderer returned invalid PNG data", {
|
|
165
|
+
stage: NodeRenderStage.OutputValidation,
|
|
166
|
+
retryable: false
|
|
167
|
+
});
|
|
168
|
+
if (dimensions.width > limits.maxWidth || dimensions.height > limits.maxHeight) throw new NodeRenderError(NodeRenderErrorCode.DimensionLimit, `Rendered image exceeds ${String(limits.maxWidth)}x${String(limits.maxHeight)} pixels`, {
|
|
169
|
+
stage: NodeRenderStage.Layout,
|
|
170
|
+
retryable: false
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function readPngDimensions(data) {
|
|
174
|
+
if (data.byteLength < PNG_HEADER_BYTES) return;
|
|
175
|
+
for (const [index, byte] of PNG_SIGNATURE.entries()) if (data[index] !== byte) return;
|
|
176
|
+
for (const [index, byte] of PNG_IHDR.entries()) if (data[PNG_IHDR_OFFSET + index] !== byte) return;
|
|
177
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
178
|
+
const width = view.getUint32(PNG_WIDTH_OFFSET);
|
|
179
|
+
const height = view.getUint32(PNG_HEIGHT_OFFSET);
|
|
180
|
+
return width > 0 && height > 0 ? {
|
|
181
|
+
width,
|
|
182
|
+
height
|
|
183
|
+
} : void 0;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/node-options.ts
|
|
187
|
+
function snapshotNodeOptions(options, allowed, label) {
|
|
188
|
+
if (!isPlainNodeObject(options)) throw invalidNodeOptions(`${label} options must be a plain object`);
|
|
189
|
+
const allowedKeys = new Set(allowed);
|
|
190
|
+
const snapshot = {};
|
|
191
|
+
let keys;
|
|
192
|
+
try {
|
|
193
|
+
keys = Object.keys(options);
|
|
194
|
+
} catch (cause) {
|
|
195
|
+
throw invalidNodeOptions(`${label} options could not be inspected`, cause);
|
|
196
|
+
}
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
if (!allowedKeys.has(key)) throw invalidNodeOptions(`Unknown ${label} option: ${key}`);
|
|
199
|
+
try {
|
|
200
|
+
snapshot[key] = Reflect.get(options, key);
|
|
201
|
+
} catch (cause) {
|
|
202
|
+
throw invalidNodeOptions(`${label}.${key} could not be read`, cause);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return Object.freeze(snapshot);
|
|
206
|
+
}
|
|
207
|
+
function snapshotOpenNodeOptions(options, label) {
|
|
208
|
+
let keys;
|
|
209
|
+
try {
|
|
210
|
+
keys = Object.keys(options);
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
throw invalidNodeOptions(`${label} could not be inspected`, cause);
|
|
213
|
+
}
|
|
214
|
+
return snapshotNodeOptions(options, keys, label);
|
|
215
|
+
}
|
|
216
|
+
function assertNodeMethod(collaborator, method, label) {
|
|
217
|
+
try {
|
|
218
|
+
if (typeof collaborator !== "object" && typeof collaborator !== "function" || collaborator === null || typeof Reflect.get(collaborator, method) !== "function") throw new TypeError(`${method} must be a function`);
|
|
219
|
+
} catch (cause) {
|
|
220
|
+
throw invalidNodeOptions(`${label} collaborator is invalid`, cause);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function invalidNodeOptions(message, cause) {
|
|
224
|
+
return new ConfigurationError$1(message, {
|
|
225
|
+
code: "invalid_node_configuration",
|
|
226
|
+
stage: "configuration",
|
|
227
|
+
cause
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function isPlainNodeObject(value) {
|
|
231
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
232
|
+
let prototype;
|
|
233
|
+
try {
|
|
234
|
+
prototype = Object.getPrototypeOf(value);
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return prototype === Object.prototype || prototype === null;
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/katex-renderer.ts
|
|
242
|
+
const DEFAULT_MAX_SOURCE_CHARS$1 = 2e4;
|
|
243
|
+
const DEFAULT_MAX_OUTPUT_BYTES$1 = 10 * 1024 * 1024;
|
|
244
|
+
const DEFAULT_MAX_WIDTH$1 = 4096;
|
|
245
|
+
const DEFAULT_MAX_HEIGHT$1 = 4096;
|
|
246
|
+
const DEFAULT_MAX_EXPAND = 1e3;
|
|
247
|
+
const DEFAULT_MAX_SIZE = 50;
|
|
248
|
+
const DEFAULT_FONT_SIZE_PX = 20;
|
|
249
|
+
const DEFAULT_PADDING_PX = 16;
|
|
250
|
+
const DEFAULT_DEVICE_SCALE_FACTOR$1 = 2;
|
|
251
|
+
const DEFAULT_BACKGROUND_COLOR$1 = "white";
|
|
252
|
+
const DEFAULT_VIEWPORT_WIDTH$1 = 1280;
|
|
253
|
+
const DEFAULT_VIEWPORT_HEIGHT$1 = 720;
|
|
254
|
+
const KATEX_LANGUAGES = /* @__PURE__ */ new Set([
|
|
255
|
+
"katex",
|
|
256
|
+
"latex",
|
|
257
|
+
"math",
|
|
258
|
+
"tex"
|
|
259
|
+
]);
|
|
260
|
+
const KATEX_SHELL = "<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; img-src 'none'; connect-src 'none'; worker-src 'none';\"></head><body><div id=\"math\"></div></body></html>";
|
|
261
|
+
const KATEX_LAYOUT_CSS = "html,body{margin:0;padding:0}#math{display:inline-block;box-sizing:border-box}.katex-display{margin:0}";
|
|
262
|
+
/**
|
|
263
|
+
* Renders display TeX with KaTeX, local fonts, and an isolated browser screenshot.
|
|
264
|
+
*/
|
|
265
|
+
var KatexRenderer = class {
|
|
266
|
+
id;
|
|
267
|
+
output;
|
|
268
|
+
runtime;
|
|
269
|
+
assets;
|
|
270
|
+
backgroundColor;
|
|
271
|
+
deviceScaleFactor;
|
|
272
|
+
fontSizePx;
|
|
273
|
+
paddingPx;
|
|
274
|
+
maxExpand;
|
|
275
|
+
maxSize;
|
|
276
|
+
maxSourceChars;
|
|
277
|
+
imageLimits;
|
|
278
|
+
optionsFingerprint;
|
|
279
|
+
constructor(options) {
|
|
280
|
+
const validated = snapshotNodeOptions(options, [
|
|
281
|
+
"runtime",
|
|
282
|
+
"assets",
|
|
283
|
+
"backgroundColor",
|
|
284
|
+
"deviceScaleFactor",
|
|
285
|
+
"fontSizePx",
|
|
286
|
+
"paddingPx",
|
|
287
|
+
"maxExpand",
|
|
288
|
+
"maxSize",
|
|
289
|
+
"maxSourceChars",
|
|
290
|
+
"maxOutputBytes",
|
|
291
|
+
"maxWidth",
|
|
292
|
+
"maxHeight"
|
|
293
|
+
], "KatexRenderer");
|
|
294
|
+
assertNodeMethod(validated.runtime, "run", "RenderRuntime");
|
|
295
|
+
assertNodeMethod(validated.assets, "loadScript", "KatexAssetLoader");
|
|
296
|
+
assertNodeMethod(validated.assets, "loadCss", "KatexAssetLoader");
|
|
297
|
+
this.id = "katex";
|
|
298
|
+
this.output = "bytes";
|
|
299
|
+
this.runtime = validated.runtime;
|
|
300
|
+
this.assets = validated.assets;
|
|
301
|
+
this.backgroundColor = validated.backgroundColor ?? DEFAULT_BACKGROUND_COLOR$1;
|
|
302
|
+
this.deviceScaleFactor = validated.deviceScaleFactor ?? DEFAULT_DEVICE_SCALE_FACTOR$1;
|
|
303
|
+
this.fontSizePx = validated.fontSizePx ?? DEFAULT_FONT_SIZE_PX;
|
|
304
|
+
this.paddingPx = validated.paddingPx ?? DEFAULT_PADDING_PX;
|
|
305
|
+
this.maxExpand = validated.maxExpand ?? DEFAULT_MAX_EXPAND;
|
|
306
|
+
this.maxSize = validated.maxSize ?? DEFAULT_MAX_SIZE;
|
|
307
|
+
this.maxSourceChars = validated.maxSourceChars ?? DEFAULT_MAX_SOURCE_CHARS$1;
|
|
308
|
+
this.imageLimits = {
|
|
309
|
+
maxOutputBytes: validated.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$1,
|
|
310
|
+
maxWidth: validated.maxWidth ?? DEFAULT_MAX_WIDTH$1,
|
|
311
|
+
maxHeight: validated.maxHeight ?? DEFAULT_MAX_HEIGHT$1
|
|
312
|
+
};
|
|
313
|
+
this.optionsFingerprint = [
|
|
314
|
+
this.backgroundColor,
|
|
315
|
+
String(this.deviceScaleFactor),
|
|
316
|
+
String(this.fontSizePx),
|
|
317
|
+
String(this.paddingPx),
|
|
318
|
+
String(this.maxExpand),
|
|
319
|
+
String(this.maxSize),
|
|
320
|
+
String(this.maxSourceChars),
|
|
321
|
+
String(this.imageLimits.maxOutputBytes),
|
|
322
|
+
String(this.imageLimits.maxWidth),
|
|
323
|
+
String(this.imageLimits.maxHeight)
|
|
324
|
+
].join(":");
|
|
325
|
+
validateOptions$1({
|
|
326
|
+
backgroundColor: this.backgroundColor,
|
|
327
|
+
deviceScaleFactor: this.deviceScaleFactor,
|
|
328
|
+
fontSizePx: this.fontSizePx,
|
|
329
|
+
paddingPx: this.paddingPx,
|
|
330
|
+
maxExpand: this.maxExpand,
|
|
331
|
+
maxSize: this.maxSize,
|
|
332
|
+
maxSourceChars: this.maxSourceChars,
|
|
333
|
+
imageLimits: this.imageLimits
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
canRender(request) {
|
|
337
|
+
return KATEX_LANGUAGES.has(request.lang.trim().toLowerCase());
|
|
338
|
+
}
|
|
339
|
+
async render(request, context) {
|
|
340
|
+
if (!this.canRender(request)) return null;
|
|
341
|
+
assertSourceLimit(request.source, this.maxSourceChars);
|
|
342
|
+
let data;
|
|
343
|
+
try {
|
|
344
|
+
data = await this.runtime.run(async (context) => {
|
|
345
|
+
const script = await this.assets.loadScript();
|
|
346
|
+
const css = await this.assets.loadCss();
|
|
347
|
+
const page = await context.newPage();
|
|
348
|
+
await page.setViewport({
|
|
349
|
+
width: DEFAULT_VIEWPORT_WIDTH$1,
|
|
350
|
+
height: DEFAULT_VIEWPORT_HEIGHT$1,
|
|
351
|
+
deviceScaleFactor: this.deviceScaleFactor
|
|
352
|
+
});
|
|
353
|
+
await page.setContent(KATEX_SHELL, { waitUntil: "domcontentloaded" });
|
|
354
|
+
await page.addStyleTag({ content: `${css}\n${KATEX_LAYOUT_CSS}` });
|
|
355
|
+
await page.addScriptTag({ content: script });
|
|
356
|
+
await page.$eval("body", (body, backgroundColor) => {
|
|
357
|
+
body.style.background = backgroundColor;
|
|
358
|
+
}, this.backgroundColor);
|
|
359
|
+
await page.$eval("#math", (container, values) => {
|
|
360
|
+
const katex = globalThis.katex;
|
|
361
|
+
if (katex === void 0) throw new Error("KaTeX browser harness failed to initialize");
|
|
362
|
+
container.innerHTML = katex.renderToString(values.source, {
|
|
363
|
+
displayMode: true,
|
|
364
|
+
globalGroup: false,
|
|
365
|
+
maxExpand: values.maxExpand,
|
|
366
|
+
maxSize: values.maxSize,
|
|
367
|
+
output: "html",
|
|
368
|
+
strict: "ignore",
|
|
369
|
+
throwOnError: true,
|
|
370
|
+
trust: false
|
|
371
|
+
});
|
|
372
|
+
container.setAttribute("style", `font-size:${String(values.fontSizePx)}px;padding:${String(values.paddingPx)}px`);
|
|
373
|
+
}, {
|
|
374
|
+
source: request.source,
|
|
375
|
+
maxExpand: this.maxExpand,
|
|
376
|
+
maxSize: this.maxSize,
|
|
377
|
+
fontSizePx: this.fontSizePx,
|
|
378
|
+
paddingPx: this.paddingPx
|
|
379
|
+
});
|
|
380
|
+
await page.evaluate(async () => {
|
|
381
|
+
await document.fonts.ready;
|
|
382
|
+
});
|
|
383
|
+
const element = await page.$("#math");
|
|
384
|
+
if (element === null) throw new Error("KaTeX screenshot element is missing");
|
|
385
|
+
const box = await element.boundingBox();
|
|
386
|
+
if (box === null) throw new Error("KaTeX screenshot element has no dimensions");
|
|
387
|
+
const pixelWidth = Math.ceil(box.width * this.deviceScaleFactor);
|
|
388
|
+
const pixelHeight = Math.ceil(box.height * this.deviceScaleFactor);
|
|
389
|
+
if (pixelWidth > this.imageLimits.maxWidth || pixelHeight > this.imageLimits.maxHeight) throw new NodeRenderError(NodeRenderErrorCode.DimensionLimit, `Rendered image exceeds ${String(this.imageLimits.maxWidth)}x${String(this.imageLimits.maxHeight)} pixels`, {
|
|
390
|
+
stage: NodeRenderStage.Layout,
|
|
391
|
+
retryable: false
|
|
392
|
+
});
|
|
393
|
+
return element.screenshot({
|
|
394
|
+
type: "png",
|
|
395
|
+
omitBackground: this.backgroundColor === "transparent"
|
|
396
|
+
});
|
|
397
|
+
}, context);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isOperationError(error)) throw error;
|
|
400
|
+
if (error instanceof NodeRenderError) {
|
|
401
|
+
const sanitizedRenderFailure = error.code === NodeRenderErrorCode.RenderFailed && error.cause === void 0;
|
|
402
|
+
if (error.code !== NodeRenderErrorCode.RenderFailed || sanitizedRenderFailure) throw error;
|
|
403
|
+
}
|
|
404
|
+
throw new NodeRenderError(NodeRenderErrorCode.RenderFailed, "KaTeX rendering failed", {
|
|
405
|
+
stage: "render",
|
|
406
|
+
retryable: false
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
assertPngLimits(data, this.imageLimits);
|
|
410
|
+
return {
|
|
411
|
+
kind: "bytes",
|
|
412
|
+
data,
|
|
413
|
+
mimeType: "image/png",
|
|
414
|
+
filename: deterministicPngFilename("math", request.source, this.optionsFingerprint),
|
|
415
|
+
altText: request.altText
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
function validateOptions$1(renderer) {
|
|
420
|
+
if (typeof renderer.backgroundColor !== "string" || renderer.backgroundColor.length === 0) throw invalidNodeOptions("backgroundColor must be a nonempty string");
|
|
421
|
+
validatePositiveFinite$1(renderer.deviceScaleFactor, "deviceScaleFactor");
|
|
422
|
+
validatePositiveFinite$1(renderer.fontSizePx, "fontSizePx");
|
|
423
|
+
validateNonNegativeFinite(renderer.paddingPx, "paddingPx");
|
|
424
|
+
validatePositiveInteger$3(renderer.maxExpand, "maxExpand");
|
|
425
|
+
validatePositiveFinite$1(renderer.maxSize, "maxSize");
|
|
426
|
+
validatePositiveInteger$3(renderer.maxSourceChars, "maxSourceChars");
|
|
427
|
+
validatePositiveInteger$3(renderer.imageLimits.maxOutputBytes, "maxOutputBytes");
|
|
428
|
+
validatePositiveInteger$3(renderer.imageLimits.maxWidth, "maxWidth");
|
|
429
|
+
validatePositiveInteger$3(renderer.imageLimits.maxHeight, "maxHeight");
|
|
430
|
+
}
|
|
431
|
+
function validatePositiveInteger$3(value, name) {
|
|
432
|
+
if (!Number.isInteger(value) || value <= 0) throw invalidNodeOptions(`${name} must be a positive integer`);
|
|
433
|
+
}
|
|
434
|
+
function validatePositiveFinite$1(value, name) {
|
|
435
|
+
if (!Number.isFinite(value) || value <= 0) throw invalidNodeOptions(`${name} must be positive`);
|
|
436
|
+
}
|
|
437
|
+
function validateNonNegativeFinite(value, name) {
|
|
438
|
+
if (!Number.isFinite(value) || value < 0) throw invalidNodeOptions(`${name} must be non-negative`);
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/mermaid-assets.ts
|
|
442
|
+
const resolver = createRequire(import.meta.url);
|
|
443
|
+
var LocalMermaidAssetLoader = class {
|
|
444
|
+
scriptPromise;
|
|
445
|
+
constructor() {
|
|
446
|
+
this.scriptPromise = void 0;
|
|
447
|
+
}
|
|
448
|
+
loadScript() {
|
|
449
|
+
this.scriptPromise ??= readFile(resolver.resolve("mermaid/dist/mermaid.min.js"), "utf8").catch((error) => {
|
|
450
|
+
this.scriptPromise = void 0;
|
|
451
|
+
throw new NodeRenderError(NodeRenderErrorCode.AssetMissing, "Unable to load Mermaid browser assets", {
|
|
452
|
+
stage: "asset_load",
|
|
453
|
+
retryable: false,
|
|
454
|
+
cause: error
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
return this.scriptPromise;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/mermaid-renderer.ts
|
|
462
|
+
const DEFAULT_MAX_SOURCE_CHARS = 5e4;
|
|
463
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
464
|
+
const DEFAULT_MAX_WIDTH = 4096;
|
|
465
|
+
const DEFAULT_MAX_HEIGHT = 4096;
|
|
466
|
+
const DEFAULT_MAX_EDGES = 500;
|
|
467
|
+
const DEFAULT_DEVICE_SCALE_FACTOR = 2;
|
|
468
|
+
const DEFAULT_BACKGROUND_COLOR = "white";
|
|
469
|
+
const DEFAULT_VIEWPORT_WIDTH = 1280;
|
|
470
|
+
const DEFAULT_VIEWPORT_HEIGHT = 720;
|
|
471
|
+
const MERMAID_SHELL = "<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'none'; font-src 'none'; connect-src 'none'; worker-src 'none'; object-src 'none'; frame-src 'none';\"></head><body><div id=\"diagram\"></div></body></html>";
|
|
472
|
+
const SECURE_MERMAID_KEYS = [
|
|
473
|
+
"secure",
|
|
474
|
+
"securityLevel",
|
|
475
|
+
"startOnLoad",
|
|
476
|
+
"maxTextSize",
|
|
477
|
+
"suppressErrorRendering",
|
|
478
|
+
"maxEdges",
|
|
479
|
+
"deterministicIds",
|
|
480
|
+
"deterministicIDSeed",
|
|
481
|
+
"layout"
|
|
482
|
+
];
|
|
483
|
+
const MERMAID_THEMES = /* @__PURE__ */ new Set([
|
|
484
|
+
"base",
|
|
485
|
+
"dark",
|
|
486
|
+
"default",
|
|
487
|
+
"forest",
|
|
488
|
+
"neutral"
|
|
489
|
+
]);
|
|
490
|
+
var MermaidRenderer = class {
|
|
491
|
+
id;
|
|
492
|
+
output;
|
|
493
|
+
runtime;
|
|
494
|
+
assets;
|
|
495
|
+
theme;
|
|
496
|
+
backgroundColor;
|
|
497
|
+
deviceScaleFactor;
|
|
498
|
+
maxSourceChars;
|
|
499
|
+
maxEdges;
|
|
500
|
+
imageLimits;
|
|
501
|
+
optionsFingerprint;
|
|
502
|
+
constructor(options) {
|
|
503
|
+
const validated = snapshotNodeOptions(options, [
|
|
504
|
+
"runtime",
|
|
505
|
+
"assets",
|
|
506
|
+
"theme",
|
|
507
|
+
"backgroundColor",
|
|
508
|
+
"deviceScaleFactor",
|
|
509
|
+
"maxSourceChars",
|
|
510
|
+
"maxEdges",
|
|
511
|
+
"maxOutputBytes",
|
|
512
|
+
"maxWidth",
|
|
513
|
+
"maxHeight"
|
|
514
|
+
], "MermaidRenderer");
|
|
515
|
+
assertNodeMethod(validated.runtime, "run", "RenderRuntime");
|
|
516
|
+
assertNodeMethod(validated.assets, "loadScript", "MermaidAssetLoader");
|
|
517
|
+
this.id = "mermaid";
|
|
518
|
+
this.output = "bytes";
|
|
519
|
+
this.runtime = validated.runtime;
|
|
520
|
+
this.assets = validated.assets;
|
|
521
|
+
this.theme = validated.theme ?? "default";
|
|
522
|
+
this.backgroundColor = validated.backgroundColor ?? DEFAULT_BACKGROUND_COLOR;
|
|
523
|
+
this.deviceScaleFactor = validated.deviceScaleFactor ?? DEFAULT_DEVICE_SCALE_FACTOR;
|
|
524
|
+
this.maxSourceChars = validated.maxSourceChars ?? DEFAULT_MAX_SOURCE_CHARS;
|
|
525
|
+
this.maxEdges = validated.maxEdges ?? DEFAULT_MAX_EDGES;
|
|
526
|
+
this.imageLimits = {
|
|
527
|
+
maxOutputBytes: validated.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
528
|
+
maxWidth: validated.maxWidth ?? DEFAULT_MAX_WIDTH,
|
|
529
|
+
maxHeight: validated.maxHeight ?? DEFAULT_MAX_HEIGHT
|
|
530
|
+
};
|
|
531
|
+
this.optionsFingerprint = [
|
|
532
|
+
this.theme,
|
|
533
|
+
this.backgroundColor,
|
|
534
|
+
String(this.deviceScaleFactor),
|
|
535
|
+
String(this.maxSourceChars),
|
|
536
|
+
String(this.maxEdges),
|
|
537
|
+
String(this.imageLimits.maxOutputBytes),
|
|
538
|
+
String(this.imageLimits.maxWidth),
|
|
539
|
+
String(this.imageLimits.maxHeight)
|
|
540
|
+
].join(":");
|
|
541
|
+
validateOptions({
|
|
542
|
+
theme: this.theme,
|
|
543
|
+
backgroundColor: this.backgroundColor,
|
|
544
|
+
deviceScaleFactor: this.deviceScaleFactor,
|
|
545
|
+
maxSourceChars: this.maxSourceChars,
|
|
546
|
+
maxEdges: this.maxEdges,
|
|
547
|
+
imageLimits: this.imageLimits
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
canRender(request) {
|
|
551
|
+
return request.lang.trim().toLowerCase() === "mermaid";
|
|
552
|
+
}
|
|
553
|
+
async render(request, context) {
|
|
554
|
+
if (!this.canRender(request)) return null;
|
|
555
|
+
assertSourceLimit(request.source, this.maxSourceChars);
|
|
556
|
+
let data;
|
|
557
|
+
try {
|
|
558
|
+
data = await this.runtime.run(async (context) => {
|
|
559
|
+
const script = await this.assets.loadScript();
|
|
560
|
+
const page = await context.newPage();
|
|
561
|
+
await page.setRequestInterception(true);
|
|
562
|
+
page.on("request", blockRequest);
|
|
563
|
+
await page.setViewport({
|
|
564
|
+
width: DEFAULT_VIEWPORT_WIDTH,
|
|
565
|
+
height: DEFAULT_VIEWPORT_HEIGHT,
|
|
566
|
+
deviceScaleFactor: this.deviceScaleFactor
|
|
567
|
+
});
|
|
568
|
+
await page.setContent(MERMAID_SHELL, { waitUntil: "domcontentloaded" });
|
|
569
|
+
await page.addScriptTag({ content: script });
|
|
570
|
+
await page.evaluate(async (input) => {
|
|
571
|
+
const mermaid = globalThis.mermaid;
|
|
572
|
+
const container = document.querySelector("#diagram");
|
|
573
|
+
if (mermaid === void 0 || container === null) throw new Error("Mermaid browser harness failed to initialize");
|
|
574
|
+
mermaid.initialize({
|
|
575
|
+
deterministicIds: true,
|
|
576
|
+
deterministicIDSeed: "slackmark-node",
|
|
577
|
+
layout: "dagre",
|
|
578
|
+
logLevel: "fatal",
|
|
579
|
+
maxEdges: input.maxEdges,
|
|
580
|
+
maxTextSize: input.maxSourceChars,
|
|
581
|
+
secure: input.secureKeys,
|
|
582
|
+
securityLevel: "strict",
|
|
583
|
+
startOnLoad: false,
|
|
584
|
+
suppressErrorRendering: true,
|
|
585
|
+
theme: input.theme
|
|
586
|
+
});
|
|
587
|
+
await mermaid.parse(input.source, { suppressErrors: false });
|
|
588
|
+
container.innerHTML = (await mermaid.render("slackmark-diagram", input.source, container)).svg;
|
|
589
|
+
document.body.style.background = input.backgroundColor;
|
|
590
|
+
}, {
|
|
591
|
+
source: request.source,
|
|
592
|
+
theme: this.theme,
|
|
593
|
+
backgroundColor: this.backgroundColor,
|
|
594
|
+
maxSourceChars: this.maxSourceChars,
|
|
595
|
+
maxEdges: this.maxEdges,
|
|
596
|
+
secureKeys: SECURE_MERMAID_KEYS
|
|
597
|
+
});
|
|
598
|
+
const element = await page.$("#diagram svg");
|
|
599
|
+
if (element === null) throw new Error("Mermaid produced no SVG");
|
|
600
|
+
const box = await element.boundingBox();
|
|
601
|
+
if (box === null) throw new Error("Mermaid SVG has no dimensions");
|
|
602
|
+
const pixelWidth = Math.ceil(box.width * this.deviceScaleFactor);
|
|
603
|
+
const pixelHeight = Math.ceil(box.height * this.deviceScaleFactor);
|
|
604
|
+
if (pixelWidth > this.imageLimits.maxWidth || pixelHeight > this.imageLimits.maxHeight) throw new NodeRenderError(NodeRenderErrorCode.DimensionLimit, "Rendered Mermaid image exceeds configured dimensions", {
|
|
605
|
+
stage: "layout",
|
|
606
|
+
retryable: false
|
|
607
|
+
});
|
|
608
|
+
return element.screenshot({
|
|
609
|
+
type: "png",
|
|
610
|
+
omitBackground: this.backgroundColor === "transparent"
|
|
611
|
+
});
|
|
612
|
+
}, context);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (isOperationError(error)) throw error;
|
|
615
|
+
if (error instanceof NodeRenderError) {
|
|
616
|
+
if (error.code !== NodeRenderErrorCode.RenderFailed) throw error;
|
|
617
|
+
}
|
|
618
|
+
throw new NodeRenderError(NodeRenderErrorCode.RenderFailed, "Mermaid rendering failed", {
|
|
619
|
+
stage: "render",
|
|
620
|
+
retryable: false
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
assertPngLimits(data, this.imageLimits);
|
|
624
|
+
return {
|
|
625
|
+
kind: "bytes",
|
|
626
|
+
data,
|
|
627
|
+
mimeType: "image/png",
|
|
628
|
+
filename: deterministicPngFilename("mermaid", request.source, this.optionsFingerprint),
|
|
629
|
+
altText: request.altText
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
function blockRequest(request) {
|
|
634
|
+
request.abort("blockedbyclient");
|
|
635
|
+
}
|
|
636
|
+
function validateOptions(renderer) {
|
|
637
|
+
if (typeof renderer.theme !== "string" || !MERMAID_THEMES.has(renderer.theme)) throw invalidNodeOptions("theme must be base, dark, default, forest, or neutral");
|
|
638
|
+
if (typeof renderer.backgroundColor !== "string" || renderer.backgroundColor.length === 0) throw invalidNodeOptions("backgroundColor must be a nonempty string");
|
|
639
|
+
validatePositiveFinite(renderer.deviceScaleFactor, "deviceScaleFactor");
|
|
640
|
+
validatePositiveInteger$2(renderer.maxSourceChars, "maxSourceChars");
|
|
641
|
+
validatePositiveInteger$2(renderer.maxEdges, "maxEdges");
|
|
642
|
+
validatePositiveInteger$2(renderer.imageLimits.maxOutputBytes, "maxOutputBytes");
|
|
643
|
+
validatePositiveInteger$2(renderer.imageLimits.maxWidth, "maxWidth");
|
|
644
|
+
validatePositiveInteger$2(renderer.imageLimits.maxHeight, "maxHeight");
|
|
645
|
+
}
|
|
646
|
+
function validatePositiveInteger$2(value, name) {
|
|
647
|
+
if (!Number.isInteger(value) || value <= 0) throw invalidNodeOptions(`${name} must be a positive integer`);
|
|
648
|
+
}
|
|
649
|
+
function validatePositiveFinite(value, name) {
|
|
650
|
+
if (!Number.isFinite(value) || value <= 0) throw invalidNodeOptions(`${name} must be positive`);
|
|
651
|
+
}
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/operation-control.ts
|
|
654
|
+
var OperationScope = class {
|
|
655
|
+
context;
|
|
656
|
+
parentSignal;
|
|
657
|
+
controller;
|
|
658
|
+
abortFromParent;
|
|
659
|
+
deadlineTimer;
|
|
660
|
+
constructor(context, stage) {
|
|
661
|
+
this.parentSignal = context.signal;
|
|
662
|
+
this.controller = new AbortController();
|
|
663
|
+
this.abortFromParent = () => {
|
|
664
|
+
const reason = this.parentSignal?.reason;
|
|
665
|
+
this.controller.abort(isOperationError(reason) ? cloneOperationError(reason, stage) : new OperationAbortedError$1(stage, { cause: reason }));
|
|
666
|
+
};
|
|
667
|
+
if (this.parentSignal?.aborted === true) this.abortFromParent();
|
|
668
|
+
else this.parentSignal?.addEventListener("abort", this.abortFromParent, { once: true });
|
|
669
|
+
if (context.deadlineMs === void 0) this.deadlineTimer = void 0;
|
|
670
|
+
else if (context.deadlineMs <= Date.now()) {
|
|
671
|
+
this.deadlineTimer = void 0;
|
|
672
|
+
this.controller.abort(new OperationDeadlineExceededError$1(stage));
|
|
673
|
+
} else this.deadlineTimer = setTimeout(() => {
|
|
674
|
+
this.controller.abort(new OperationDeadlineExceededError$1(stage));
|
|
675
|
+
}, context.deadlineMs - Date.now());
|
|
676
|
+
this.context = Object.freeze({
|
|
677
|
+
signal: this.controller.signal,
|
|
678
|
+
deadlineMs: context.deadlineMs
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
abortForShutdown(stage) {
|
|
682
|
+
this.controller.abort(new OperationShutdownError$1(stage));
|
|
683
|
+
}
|
|
684
|
+
abort(reason) {
|
|
685
|
+
this.controller.abort(reason);
|
|
686
|
+
}
|
|
687
|
+
race(promise) {
|
|
688
|
+
return raceWithContext(promise, this.context);
|
|
689
|
+
}
|
|
690
|
+
cleanup() {
|
|
691
|
+
if (this.deadlineTimer !== void 0) clearTimeout(this.deadlineTimer);
|
|
692
|
+
this.parentSignal?.removeEventListener("abort", this.abortFromParent);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
function createOperationScope(options = {}, stage) {
|
|
696
|
+
validateOperationOptions(options);
|
|
697
|
+
return new OperationScope({
|
|
698
|
+
signal: options.signal,
|
|
699
|
+
deadlineMs: options.timeoutMs === void 0 ? void 0 : Date.now() + options.timeoutMs
|
|
700
|
+
}, stage);
|
|
701
|
+
}
|
|
702
|
+
function raceWithContext(promise, context) {
|
|
703
|
+
const signal = context.signal;
|
|
704
|
+
if (signal?.aborted === true) return Promise.reject(signal.reason ?? new OperationAbortedError$1("operation"));
|
|
705
|
+
return new Promise((resolve, reject) => {
|
|
706
|
+
const onAbort = () => {
|
|
707
|
+
signal?.removeEventListener("abort", onAbort);
|
|
708
|
+
reject(signal?.reason ?? new OperationAbortedError$1("operation"));
|
|
709
|
+
};
|
|
710
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
711
|
+
promise.then((value) => {
|
|
712
|
+
signal?.removeEventListener("abort", onAbort);
|
|
713
|
+
resolve(value);
|
|
714
|
+
}, (error) => {
|
|
715
|
+
signal?.removeEventListener("abort", onAbort);
|
|
716
|
+
reject(error);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function operationReason(signal) {
|
|
721
|
+
return isOperationError(signal.reason) ? signal.reason : new OperationAbortedError$1("operation", { cause: signal.reason });
|
|
722
|
+
}
|
|
723
|
+
function validateOperationOptions(options) {
|
|
724
|
+
if (!isPlainObject(options)) throw new ConfigurationError$1("Operation options must be a plain object", {
|
|
725
|
+
code: "invalid_operation_options",
|
|
726
|
+
stage: "configuration"
|
|
727
|
+
});
|
|
728
|
+
for (const key of Object.keys(options)) if (key !== "signal" && key !== "timeoutMs") throw new ConfigurationError$1(`Unknown operation option: ${key}`, {
|
|
729
|
+
code: "invalid_operation_options",
|
|
730
|
+
stage: "configuration"
|
|
731
|
+
});
|
|
732
|
+
if (options.signal !== void 0 && (typeof options.signal !== "object" || options.signal === null || typeof options.signal.addEventListener !== "function" || typeof options.signal.removeEventListener !== "function")) throw new ConfigurationError$1("signal must be an AbortSignal", {
|
|
733
|
+
code: "invalid_signal",
|
|
734
|
+
stage: "configuration"
|
|
735
|
+
});
|
|
736
|
+
if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) throw new ConfigurationError$1("timeoutMs must be positive", {
|
|
737
|
+
code: "invalid_timeout",
|
|
738
|
+
stage: "configuration"
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function isPlainObject(value) {
|
|
742
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
743
|
+
const prototype = Object.getPrototypeOf(value);
|
|
744
|
+
return prototype === Object.prototype || prototype === null;
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/puppeteer-render-runtime.ts
|
|
748
|
+
const DEFAULT_CONCURRENCY = 2;
|
|
749
|
+
const DEFAULT_MAX_QUEUE_SIZE = 32;
|
|
750
|
+
const INTERNAL_LAUNCH_TIMEOUT_MS = 1e4;
|
|
751
|
+
const INTERNAL_CLEANUP_TIMEOUT_MS = 2e3;
|
|
752
|
+
const DENY_NETWORK_PROXY = "http://127.0.0.1:9";
|
|
753
|
+
var PuppeteerRenderRuntime = class {
|
|
754
|
+
launcher;
|
|
755
|
+
launchOptions;
|
|
756
|
+
concurrency;
|
|
757
|
+
maxQueueSize;
|
|
758
|
+
contextOptions;
|
|
759
|
+
pendingJobs;
|
|
760
|
+
activeScopes;
|
|
761
|
+
idleResolvers;
|
|
762
|
+
activeJobs;
|
|
763
|
+
browserPromise;
|
|
764
|
+
currentBrowser;
|
|
765
|
+
launchController;
|
|
766
|
+
launchWaiters;
|
|
767
|
+
launchPending;
|
|
768
|
+
launchAbandoned;
|
|
769
|
+
launchError;
|
|
770
|
+
forceCloseBrowser;
|
|
771
|
+
forceClosePromise;
|
|
772
|
+
closePromise;
|
|
773
|
+
closeBehavior;
|
|
774
|
+
accepting;
|
|
775
|
+
constructor(options) {
|
|
776
|
+
const validated = snapshotNodeOptions(options, [
|
|
777
|
+
"launcher",
|
|
778
|
+
"launchOptions",
|
|
779
|
+
"concurrency",
|
|
780
|
+
"maxQueueSize"
|
|
781
|
+
], "PuppeteerRenderRuntime");
|
|
782
|
+
assertNodeMethod(validated.launcher, "launch", "BrowserLauncher");
|
|
783
|
+
const launchOptions = validated.launchOptions === void 0 ? void 0 : snapshotOpenNodeOptions(validated.launchOptions, "Puppeteer launchOptions");
|
|
784
|
+
this.launcher = validated.launcher;
|
|
785
|
+
this.launchOptions = {
|
|
786
|
+
headless: "shell",
|
|
787
|
+
...launchOptions
|
|
788
|
+
};
|
|
789
|
+
this.concurrency = validated.concurrency ?? DEFAULT_CONCURRENCY;
|
|
790
|
+
this.maxQueueSize = validated.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
791
|
+
this.contextOptions = {
|
|
792
|
+
proxyServer: DENY_NETWORK_PROXY,
|
|
793
|
+
proxyBypassList: ["<-loopback>"],
|
|
794
|
+
downloadBehavior: { policy: "deny" }
|
|
795
|
+
};
|
|
796
|
+
this.pendingJobs = [];
|
|
797
|
+
this.activeScopes = /* @__PURE__ */ new Set();
|
|
798
|
+
this.idleResolvers = [];
|
|
799
|
+
this.activeJobs = 0;
|
|
800
|
+
this.browserPromise = void 0;
|
|
801
|
+
this.currentBrowser = void 0;
|
|
802
|
+
this.launchController = void 0;
|
|
803
|
+
this.launchWaiters = /* @__PURE__ */ new Set();
|
|
804
|
+
this.launchPending = false;
|
|
805
|
+
this.launchAbandoned = false;
|
|
806
|
+
this.launchError = void 0;
|
|
807
|
+
this.forceCloseBrowser = void 0;
|
|
808
|
+
this.forceClosePromise = void 0;
|
|
809
|
+
this.closePromise = void 0;
|
|
810
|
+
this.closeBehavior = void 0;
|
|
811
|
+
this.accepting = true;
|
|
812
|
+
validatePositiveInteger$1(this.concurrency, "concurrency");
|
|
813
|
+
validateNonNegativeInteger$1(this.maxQueueSize, "maxQueueSize");
|
|
814
|
+
}
|
|
815
|
+
run(job, operation) {
|
|
816
|
+
if (!this.accepting) return Promise.reject(new OperationShutdownError$1("render_admission"));
|
|
817
|
+
if (this.activeJobs >= this.concurrency && this.pendingJobs.length >= this.maxQueueSize) return Promise.reject(new NodeRenderError(NodeRenderErrorCode.QueueFull, `Render queue is full (max ${String(this.maxQueueSize)} pending)`, {
|
|
818
|
+
stage: "admission",
|
|
819
|
+
retryable: true
|
|
820
|
+
}));
|
|
821
|
+
return new Promise((resolve, reject) => {
|
|
822
|
+
const scope = new OperationScope(operation, "render");
|
|
823
|
+
let pending;
|
|
824
|
+
const abortWhileQueued = () => {
|
|
825
|
+
const index = this.pendingJobs.indexOf(pending);
|
|
826
|
+
if (index >= 0) {
|
|
827
|
+
this.pendingJobs.splice(index, 1);
|
|
828
|
+
pending.reject(operationReason(requireSignal$1(scope.context)));
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
pending = {
|
|
832
|
+
start: () => {
|
|
833
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
834
|
+
this.activeScopes.add(scope);
|
|
835
|
+
this.startUnderlying(job, scope, resolve, reject);
|
|
836
|
+
},
|
|
837
|
+
reject: (error) => {
|
|
838
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
839
|
+
scope.cleanup();
|
|
840
|
+
reject(error);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
if (scope.context.signal?.aborted === true) {
|
|
844
|
+
pending.reject(operationReason(requireSignal$1(scope.context)));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (this.activeJobs < this.concurrency) {
|
|
848
|
+
this.activeJobs += 1;
|
|
849
|
+
pending.start();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
scope.context.signal?.addEventListener("abort", abortWhileQueued, { once: true });
|
|
853
|
+
this.pendingJobs.push(pending);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
warmup(operation) {
|
|
857
|
+
return this.run(async () => void 0, operation);
|
|
858
|
+
}
|
|
859
|
+
close(operation, behavior = "cancel") {
|
|
860
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
861
|
+
const closeScope = new OperationScope(operation, "browser_close");
|
|
862
|
+
this.accepting = false;
|
|
863
|
+
this.closeBehavior = behavior;
|
|
864
|
+
if (behavior === "cancel") {
|
|
865
|
+
const shutdown = new OperationShutdownError$1("render");
|
|
866
|
+
for (const pending of this.pendingJobs.splice(0)) pending.reject(shutdown);
|
|
867
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("render");
|
|
868
|
+
this.abandonLaunch(shutdown);
|
|
869
|
+
}
|
|
870
|
+
this.closePromise = this.finishClose(closeScope.context).finally(() => {
|
|
871
|
+
closeScope.cleanup();
|
|
872
|
+
});
|
|
873
|
+
return this.closePromise;
|
|
874
|
+
}
|
|
875
|
+
status() {
|
|
876
|
+
return Object.freeze({
|
|
877
|
+
accepting: this.accepting,
|
|
878
|
+
active: this.activeJobs,
|
|
879
|
+
queued: this.pendingJobs.length,
|
|
880
|
+
retainedBytes: 0
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
startUnderlying(job, scope, resolve, reject) {
|
|
884
|
+
const underlying = this.executeUnderlying(job, scope);
|
|
885
|
+
scope.race(underlying).then(resolve, reject);
|
|
886
|
+
underlying.then(() => {
|
|
887
|
+
this.finishUnderlying(scope);
|
|
888
|
+
}, () => {
|
|
889
|
+
this.finishUnderlying(scope);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
async executeUnderlying(job, scope) {
|
|
893
|
+
let browser;
|
|
894
|
+
let context;
|
|
895
|
+
let contextClosePromise;
|
|
896
|
+
let contextCleanupPromise;
|
|
897
|
+
let outcome;
|
|
898
|
+
const closeContext = () => {
|
|
899
|
+
if (context === void 0) return Promise.resolve();
|
|
900
|
+
contextClosePromise ??= Promise.resolve().then(() => context.close());
|
|
901
|
+
return contextClosePromise;
|
|
902
|
+
};
|
|
903
|
+
const cleanupContext = () => {
|
|
904
|
+
contextCleanupPromise ??= cleanupWithDeadline(closeContext(), async () => {
|
|
905
|
+
await this.forceCloseBrowserOnce(browser);
|
|
906
|
+
});
|
|
907
|
+
return contextCleanupPromise;
|
|
908
|
+
};
|
|
909
|
+
const requestContextClose = () => {
|
|
910
|
+
if (context !== void 0) cleanupContext().catch(() => void 0);
|
|
911
|
+
};
|
|
912
|
+
scope.context.signal?.addEventListener("abort", requestContextClose, { once: true });
|
|
913
|
+
try {
|
|
914
|
+
browser = await this.getBrowser(scope);
|
|
915
|
+
throwIfContextEnded(scope.context);
|
|
916
|
+
try {
|
|
917
|
+
context = await browser.createBrowserContext(this.contextOptions);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (isBrowserDisconnected(error)) throw error;
|
|
920
|
+
throw new NodeRenderError(NodeRenderErrorCode.ContextFailed, "Browser context creation failed", {
|
|
921
|
+
stage: "context_create",
|
|
922
|
+
retryable: true,
|
|
923
|
+
cause: error
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
throwIfContextEnded(scope.context);
|
|
927
|
+
outcome = {
|
|
928
|
+
ok: true,
|
|
929
|
+
value: await Promise.resolve().then(() => job(context, scope.context))
|
|
930
|
+
};
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (scope.context.signal?.aborted === true) outcome = {
|
|
933
|
+
ok: false,
|
|
934
|
+
error: operationReason(requireSignal$1(scope.context))
|
|
935
|
+
};
|
|
936
|
+
else if (isBrowserDisconnected(error)) outcome = {
|
|
937
|
+
ok: false,
|
|
938
|
+
error: new NodeRenderError(NodeRenderErrorCode.BrowserDisconnected, "Chromium disconnected during rendering", {
|
|
939
|
+
stage: "browser",
|
|
940
|
+
retryable: true,
|
|
941
|
+
cause: error
|
|
942
|
+
})
|
|
943
|
+
};
|
|
944
|
+
else outcome = {
|
|
945
|
+
ok: false,
|
|
946
|
+
error: normalizeNodeRenderError(error, NodeRenderErrorCode.RenderFailed, "Browser rendering failed", "render", true)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
scope.context.signal?.removeEventListener("abort", requestContextClose);
|
|
950
|
+
if (context !== void 0) try {
|
|
951
|
+
await cleanupContext();
|
|
952
|
+
} catch (cleanupError) {
|
|
953
|
+
if (outcome.ok || cleanupError instanceof NodeRenderError && cleanupError.stage === "browser_cleanup") outcome = {
|
|
954
|
+
ok: false,
|
|
955
|
+
error: cleanupError
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (outcome.ok) return outcome.value;
|
|
959
|
+
throw outcome.error;
|
|
960
|
+
}
|
|
961
|
+
finishUnderlying(scope) {
|
|
962
|
+
scope.cleanup();
|
|
963
|
+
this.activeScopes.delete(scope);
|
|
964
|
+
this.activeJobs -= 1;
|
|
965
|
+
const next = this.pendingJobs.shift();
|
|
966
|
+
if (next !== void 0 && this.closeBehavior !== "cancel") {
|
|
967
|
+
this.activeJobs += 1;
|
|
968
|
+
next.start();
|
|
969
|
+
} else if (next !== void 0) next.reject(new OperationShutdownError$1("render"));
|
|
970
|
+
if (this.activeJobs === 0 && this.pendingJobs.length === 0) for (const resolve of this.idleResolvers.splice(0)) resolve();
|
|
971
|
+
}
|
|
972
|
+
getBrowser(scope) {
|
|
973
|
+
if (this.browserPromise !== void 0) {
|
|
974
|
+
if (this.launchPending) {
|
|
975
|
+
this.launchWaiters.add(scope);
|
|
976
|
+
if (this.launchError !== void 0) scope.abort(this.launchError);
|
|
977
|
+
}
|
|
978
|
+
return this.browserPromise;
|
|
979
|
+
}
|
|
980
|
+
this.forceCloseBrowser = void 0;
|
|
981
|
+
this.forceClosePromise = void 0;
|
|
982
|
+
this.launchPending = true;
|
|
983
|
+
this.launchAbandoned = false;
|
|
984
|
+
this.launchError = void 0;
|
|
985
|
+
this.launchWaiters.add(scope);
|
|
986
|
+
const launchController = new AbortController();
|
|
987
|
+
this.launchController = launchController;
|
|
988
|
+
const deadlineMs = Date.now() + INTERNAL_LAUNCH_TIMEOUT_MS;
|
|
989
|
+
const launchOperation = {
|
|
990
|
+
signal: launchController.signal,
|
|
991
|
+
deadlineMs
|
|
992
|
+
};
|
|
993
|
+
const timeout = setTimeout(() => {
|
|
994
|
+
const error = new NodeRenderError(NodeRenderErrorCode.BrowserFailed, "Chromium launch deadline exceeded", {
|
|
995
|
+
stage: "browser_launch",
|
|
996
|
+
retryable: true
|
|
997
|
+
});
|
|
998
|
+
this.abandonLaunch(error);
|
|
999
|
+
}, INTERNAL_LAUNCH_TIMEOUT_MS);
|
|
1000
|
+
const launched = Promise.resolve().then(() => this.launcher.launch(this.launchOptions, launchOperation));
|
|
1001
|
+
let tracked;
|
|
1002
|
+
tracked = Promise.resolve(launched).then(async (browser) => {
|
|
1003
|
+
clearTimeout(timeout);
|
|
1004
|
+
this.launchPending = false;
|
|
1005
|
+
this.launchWaiters.clear();
|
|
1006
|
+
if (this.launchAbandoned || !this.accepting) {
|
|
1007
|
+
await this.forceCloseHandleOnce(browser);
|
|
1008
|
+
if (this.browserPromise === tracked) this.browserPromise = void 0;
|
|
1009
|
+
return browser;
|
|
1010
|
+
}
|
|
1011
|
+
this.currentBrowser = browser;
|
|
1012
|
+
browser.on("disconnected", () => {
|
|
1013
|
+
if (this.browserPromise === tracked) {
|
|
1014
|
+
this.browserPromise = void 0;
|
|
1015
|
+
this.currentBrowser = void 0;
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
return browser;
|
|
1019
|
+
}).catch((error) => {
|
|
1020
|
+
clearTimeout(timeout);
|
|
1021
|
+
this.launchPending = false;
|
|
1022
|
+
this.launchWaiters.clear();
|
|
1023
|
+
if (this.browserPromise === tracked) this.browserPromise = void 0;
|
|
1024
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.BrowserFailed, "Chromium launch failed", "browser_launch", true);
|
|
1025
|
+
});
|
|
1026
|
+
this.browserPromise = tracked;
|
|
1027
|
+
return tracked;
|
|
1028
|
+
}
|
|
1029
|
+
async finishClose(operation) {
|
|
1030
|
+
try {
|
|
1031
|
+
await raceWithContext(this.waitForIdle(), operation);
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
this.closeBehavior = "cancel";
|
|
1034
|
+
const shutdown = new OperationShutdownError$1("render");
|
|
1035
|
+
for (const pending of this.pendingJobs.splice(0)) pending.reject(shutdown);
|
|
1036
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("render");
|
|
1037
|
+
await this.forceCloseBrowserOnce();
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
const browserPromise = this.browserPromise;
|
|
1041
|
+
if (browserPromise === void 0) return;
|
|
1042
|
+
let browser;
|
|
1043
|
+
try {
|
|
1044
|
+
browser = await raceWithContext(browserPromise, operation);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (isOperationError(error) && error.code === OperationErrorCode$1.Shutdown) return;
|
|
1047
|
+
await this.forceCloseBrowserOnce();
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
if (!browser.connected) return;
|
|
1051
|
+
try {
|
|
1052
|
+
await raceWithContext(cleanupWithDeadline(browser.close(), () => this.forceCloseBrowserOnce(browser)), operation);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
await this.forceCloseBrowserOnce(browser);
|
|
1055
|
+
if (isOperationError(error)) throw error;
|
|
1056
|
+
throw new NodeRenderError(NodeRenderErrorCode.CloseFailed, "Chromium close failed", {
|
|
1057
|
+
stage: "browser_close",
|
|
1058
|
+
retryable: true,
|
|
1059
|
+
cause: error
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
waitForIdle() {
|
|
1064
|
+
if (this.activeJobs === 0 && this.pendingJobs.length === 0) return Promise.resolve();
|
|
1065
|
+
return new Promise((resolve) => {
|
|
1066
|
+
this.idleResolvers.push(resolve);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
forceCloseBrowserOnce(browser) {
|
|
1070
|
+
if (this.launchPending) {
|
|
1071
|
+
this.abandonLaunch(new OperationShutdownError$1("browser_close"));
|
|
1072
|
+
return Promise.resolve();
|
|
1073
|
+
}
|
|
1074
|
+
if (browser !== void 0) return this.forceCloseHandleOnce(browser);
|
|
1075
|
+
const browserPromise = this.browserPromise;
|
|
1076
|
+
return browserPromise === void 0 ? Promise.resolve() : browserPromise.then((handle) => this.forceCloseHandleOnce(handle), () => void 0);
|
|
1077
|
+
}
|
|
1078
|
+
abandonLaunch(error) {
|
|
1079
|
+
if (!this.launchPending) return;
|
|
1080
|
+
this.launchAbandoned = true;
|
|
1081
|
+
this.launchError = error;
|
|
1082
|
+
this.launchController?.abort(error);
|
|
1083
|
+
for (const waiter of this.launchWaiters) waiter.abort(error);
|
|
1084
|
+
}
|
|
1085
|
+
forceCloseHandleOnce(browser) {
|
|
1086
|
+
if (this.currentBrowser === browser) {
|
|
1087
|
+
this.currentBrowser = void 0;
|
|
1088
|
+
this.browserPromise = void 0;
|
|
1089
|
+
}
|
|
1090
|
+
if (this.forceCloseBrowser !== browser || this.forceClosePromise === void 0) {
|
|
1091
|
+
this.forceCloseBrowser = browser;
|
|
1092
|
+
this.forceClosePromise = browser.forceClose().catch((error) => {
|
|
1093
|
+
throw new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Browser cleanup failed", {
|
|
1094
|
+
stage: "browser_cleanup",
|
|
1095
|
+
retryable: true,
|
|
1096
|
+
cause: error
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
return this.forceClosePromise;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
async function importPuppeteerAndLaunch(options) {
|
|
1104
|
+
const { launch } = await import("puppeteer");
|
|
1105
|
+
return new PuppeteerBrowserHandle(await launch(options));
|
|
1106
|
+
}
|
|
1107
|
+
var PuppeteerBrowserLauncher = class {
|
|
1108
|
+
launchBrowser;
|
|
1109
|
+
constructor(launchBrowser = importPuppeteerAndLaunch) {
|
|
1110
|
+
this.launchBrowser = launchBrowser;
|
|
1111
|
+
}
|
|
1112
|
+
async launch(options, operation) {
|
|
1113
|
+
const deadlineMs = operation.deadlineMs ?? Date.now() + INTERNAL_LAUNCH_TIMEOUT_MS;
|
|
1114
|
+
const timeout = Math.max(1, deadlineMs - Date.now());
|
|
1115
|
+
try {
|
|
1116
|
+
const launching = this.launchBrowser({
|
|
1117
|
+
...options,
|
|
1118
|
+
timeout: Math.min(options.timeout ?? timeout, timeout)
|
|
1119
|
+
});
|
|
1120
|
+
try {
|
|
1121
|
+
return await raceWithContext(launching, operation);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
await launching.then((handle) => handle.forceClose().catch(() => void 0), () => void 0);
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
if (isMissingBrowserError(error)) throw new NodeRenderError(NodeRenderErrorCode.BrowserBinaryMissing, "Chromium executable is unavailable", {
|
|
1128
|
+
stage: "browser_launch",
|
|
1129
|
+
retryable: false,
|
|
1130
|
+
cause: error
|
|
1131
|
+
});
|
|
1132
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.BrowserFailed, "Chromium launch failed", "browser_launch", true);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
var PuppeteerBrowserHandle = class {
|
|
1137
|
+
browser;
|
|
1138
|
+
constructor(browser) {
|
|
1139
|
+
this.browser = browser;
|
|
1140
|
+
}
|
|
1141
|
+
get connected() {
|
|
1142
|
+
return this.browser.connected;
|
|
1143
|
+
}
|
|
1144
|
+
createBrowserContext(options) {
|
|
1145
|
+
return this.browser.createBrowserContext(options);
|
|
1146
|
+
}
|
|
1147
|
+
close() {
|
|
1148
|
+
return this.browser.close();
|
|
1149
|
+
}
|
|
1150
|
+
async forceClose() {
|
|
1151
|
+
this.browser.process()?.kill("SIGKILL");
|
|
1152
|
+
try {
|
|
1153
|
+
await this.browser.disconnect();
|
|
1154
|
+
} catch {}
|
|
1155
|
+
}
|
|
1156
|
+
on(event, handler) {
|
|
1157
|
+
this.browser.on(event, handler);
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
function cleanupWithDeadline(cleanup, forceCleanup) {
|
|
1161
|
+
return new Promise((resolve, reject) => {
|
|
1162
|
+
let settled = false;
|
|
1163
|
+
let closedGracefully = false;
|
|
1164
|
+
const timeout = setTimeout(() => {
|
|
1165
|
+
if (settled) return;
|
|
1166
|
+
settled = true;
|
|
1167
|
+
forceCleanup().then(() => {
|
|
1168
|
+
if (closedGracefully) {
|
|
1169
|
+
resolve();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
reject(new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Cleanup deadline exceeded", {
|
|
1173
|
+
stage: "context_cleanup",
|
|
1174
|
+
retryable: true
|
|
1175
|
+
}));
|
|
1176
|
+
}, reject);
|
|
1177
|
+
}, INTERNAL_CLEANUP_TIMEOUT_MS);
|
|
1178
|
+
cleanup.then(() => {
|
|
1179
|
+
closedGracefully = true;
|
|
1180
|
+
if (!settled) {
|
|
1181
|
+
settled = true;
|
|
1182
|
+
clearTimeout(timeout);
|
|
1183
|
+
resolve();
|
|
1184
|
+
}
|
|
1185
|
+
}, (error) => {
|
|
1186
|
+
if (!settled) {
|
|
1187
|
+
settled = true;
|
|
1188
|
+
clearTimeout(timeout);
|
|
1189
|
+
forceCleanup().then(() => {
|
|
1190
|
+
reject(new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Browser context cleanup failed", {
|
|
1191
|
+
stage: "context_cleanup",
|
|
1192
|
+
retryable: true,
|
|
1193
|
+
cause: error
|
|
1194
|
+
}));
|
|
1195
|
+
}, reject);
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
function throwIfContextEnded(operation) {
|
|
1201
|
+
if (operation.signal?.aborted === true) throw operationReason(operation.signal);
|
|
1202
|
+
if (operation.deadlineMs !== void 0 && Date.now() >= operation.deadlineMs) throw new OperationDeadlineExceededError$1("render");
|
|
1203
|
+
}
|
|
1204
|
+
function requireSignal$1(operation) {
|
|
1205
|
+
if (operation.signal === void 0) throw new OperationShutdownError$1("operation");
|
|
1206
|
+
return operation.signal;
|
|
1207
|
+
}
|
|
1208
|
+
function isBrowserDisconnected(error) {
|
|
1209
|
+
return error instanceof Error && (error.message.includes("disconnected") || error.message.includes("Target closed") || error.message.includes("Connection closed"));
|
|
1210
|
+
}
|
|
1211
|
+
function validatePositiveInteger$1(value, name) {
|
|
1212
|
+
if (!Number.isInteger(value) || value <= 0) throw invalidNodeOptions(`${name} must be a positive integer`);
|
|
1213
|
+
}
|
|
1214
|
+
function validateNonNegativeInteger$1(value, name) {
|
|
1215
|
+
if (!Number.isInteger(value) || value < 0) throw invalidNodeOptions(`${name} must be a non-negative integer`);
|
|
1216
|
+
}
|
|
1217
|
+
function isMissingBrowserError(error) {
|
|
1218
|
+
if (!(error instanceof Error)) return false;
|
|
1219
|
+
return error.message.includes("Could not find Chrome") || error.message.includes("Browser was not found") || error.message.includes("executable doesn't exist");
|
|
1220
|
+
}
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/status.ts
|
|
1223
|
+
const NodeConverterState = {
|
|
1224
|
+
Open: "open",
|
|
1225
|
+
Closing: "closing",
|
|
1226
|
+
Closed: "closed",
|
|
1227
|
+
CloseFailed: "close-failed"
|
|
1228
|
+
};
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region src/upload-scheduler.ts
|
|
1231
|
+
var UploadScheduler = class {
|
|
1232
|
+
concurrency;
|
|
1233
|
+
maxQueueSize;
|
|
1234
|
+
pending;
|
|
1235
|
+
activeScopes;
|
|
1236
|
+
idleResolvers;
|
|
1237
|
+
active;
|
|
1238
|
+
retainedBytes;
|
|
1239
|
+
closePromise;
|
|
1240
|
+
closeBehavior;
|
|
1241
|
+
accepting;
|
|
1242
|
+
constructor(options) {
|
|
1243
|
+
this.concurrency = options.concurrency;
|
|
1244
|
+
this.maxQueueSize = options.maxQueueSize;
|
|
1245
|
+
this.pending = [];
|
|
1246
|
+
this.activeScopes = /* @__PURE__ */ new Set();
|
|
1247
|
+
this.idleResolvers = [];
|
|
1248
|
+
this.active = 0;
|
|
1249
|
+
this.retainedBytes = 0;
|
|
1250
|
+
this.closePromise = void 0;
|
|
1251
|
+
this.closeBehavior = void 0;
|
|
1252
|
+
this.accepting = true;
|
|
1253
|
+
validatePositiveInteger(this.concurrency, "upload.concurrency");
|
|
1254
|
+
validateNonNegativeInteger(this.maxQueueSize, "upload.maxQueueSize");
|
|
1255
|
+
}
|
|
1256
|
+
schedule(uploader, image, operation) {
|
|
1257
|
+
if (!this.accepting) return Promise.reject(new OperationShutdownError$1("upload_admission"));
|
|
1258
|
+
if (this.active >= this.concurrency && this.pending.length >= this.maxQueueSize) return Promise.reject(queueError());
|
|
1259
|
+
let scope;
|
|
1260
|
+
try {
|
|
1261
|
+
scope = new OperationScope(operation, "upload");
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
return Promise.reject(error);
|
|
1264
|
+
}
|
|
1265
|
+
this.retainedBytes += image.data.byteLength;
|
|
1266
|
+
return new Promise((resolve, reject) => {
|
|
1267
|
+
let pending;
|
|
1268
|
+
const abortWhileQueued = () => {
|
|
1269
|
+
const index = this.pending.indexOf(pending);
|
|
1270
|
+
if (index >= 0) {
|
|
1271
|
+
this.pending.splice(index, 1);
|
|
1272
|
+
pending.reject(operationReason(requireSignal(scope.context)));
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
pending = {
|
|
1276
|
+
start: () => {
|
|
1277
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
1278
|
+
this.activeScopes.add(scope);
|
|
1279
|
+
this.startUnderlying(uploader, image, scope, resolve, reject);
|
|
1280
|
+
},
|
|
1281
|
+
reject: (error) => {
|
|
1282
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
1283
|
+
scope.cleanup();
|
|
1284
|
+
this.retainedBytes -= image.data.byteLength;
|
|
1285
|
+
reject(error);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
if (scope.context.signal?.aborted === true) {
|
|
1289
|
+
pending.reject(operationReason(requireSignal(scope.context)));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (this.active < this.concurrency) {
|
|
1293
|
+
this.active += 1;
|
|
1294
|
+
pending.start();
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
scope.context.signal?.addEventListener("abort", abortWhileQueued, { once: true });
|
|
1298
|
+
this.pending.push(pending);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
close(operation, behavior = "cancel") {
|
|
1302
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
1303
|
+
const closeScope = new OperationScope(operation, "upload_close");
|
|
1304
|
+
this.accepting = false;
|
|
1305
|
+
this.closeBehavior = behavior;
|
|
1306
|
+
if (behavior === "cancel") {
|
|
1307
|
+
const shutdown = new OperationShutdownError$1("upload");
|
|
1308
|
+
for (const pending of this.pending.splice(0)) pending.reject(shutdown);
|
|
1309
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("upload");
|
|
1310
|
+
}
|
|
1311
|
+
this.closePromise = raceWithContext(this.waitForIdle(), closeScope.context).catch((error) => {
|
|
1312
|
+
this.closeBehavior = "cancel";
|
|
1313
|
+
const shutdown = new OperationShutdownError$1("upload");
|
|
1314
|
+
for (const pending of this.pending.splice(0)) pending.reject(shutdown);
|
|
1315
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("upload");
|
|
1316
|
+
throw error;
|
|
1317
|
+
}).finally(() => {
|
|
1318
|
+
closeScope.cleanup();
|
|
1319
|
+
});
|
|
1320
|
+
return this.closePromise;
|
|
1321
|
+
}
|
|
1322
|
+
status() {
|
|
1323
|
+
return Object.freeze({
|
|
1324
|
+
accepting: this.accepting,
|
|
1325
|
+
active: this.active,
|
|
1326
|
+
queued: this.pending.length,
|
|
1327
|
+
retainedBytes: this.retainedBytes
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
startUnderlying(uploader, image, scope, resolve, reject) {
|
|
1331
|
+
Promise.resolve().then(() => uploader.upload(image, scope.context)).then((value) => {
|
|
1332
|
+
resolve(value);
|
|
1333
|
+
this.finishUnderlying(scope, image.data.byteLength);
|
|
1334
|
+
}, (error) => {
|
|
1335
|
+
reject(error);
|
|
1336
|
+
this.finishUnderlying(scope, image.data.byteLength);
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
finishUnderlying(scope, byteLength) {
|
|
1340
|
+
scope.cleanup();
|
|
1341
|
+
this.activeScopes.delete(scope);
|
|
1342
|
+
this.active -= 1;
|
|
1343
|
+
this.retainedBytes -= byteLength;
|
|
1344
|
+
const next = this.pending.shift();
|
|
1345
|
+
if (next !== void 0 && this.closeBehavior !== "cancel") {
|
|
1346
|
+
this.active += 1;
|
|
1347
|
+
next.start();
|
|
1348
|
+
} else if (next !== void 0) next.reject(new OperationShutdownError$1("upload"));
|
|
1349
|
+
if (this.active === 0 && this.pending.length === 0) for (const resolve of this.idleResolvers.splice(0)) resolve();
|
|
1350
|
+
}
|
|
1351
|
+
waitForIdle() {
|
|
1352
|
+
if (this.active === 0 && this.pending.length === 0) return Promise.resolve();
|
|
1353
|
+
return new Promise((resolve) => {
|
|
1354
|
+
this.idleResolvers.push(resolve);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
var ScheduledSlackUploader = class {
|
|
1359
|
+
scheduler;
|
|
1360
|
+
uploader;
|
|
1361
|
+
constructor(scheduler, uploader) {
|
|
1362
|
+
this.scheduler = scheduler;
|
|
1363
|
+
this.uploader = uploader;
|
|
1364
|
+
}
|
|
1365
|
+
upload(image, operation) {
|
|
1366
|
+
return this.scheduler.schedule(this.uploader, image, operation);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
function queueError() {
|
|
1370
|
+
return new SlackUploadError("Slack upload queue is full", {
|
|
1371
|
+
stage: SlackUploadStage.Admission,
|
|
1372
|
+
code: "upload_queue_full",
|
|
1373
|
+
retryable: true
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
function requireSignal(operation) {
|
|
1377
|
+
if (operation.signal === void 0) throw new OperationShutdownError$1("upload");
|
|
1378
|
+
return operation.signal;
|
|
1379
|
+
}
|
|
1380
|
+
function validatePositiveInteger(value, name) {
|
|
1381
|
+
if (!Number.isInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive integer`);
|
|
1382
|
+
}
|
|
1383
|
+
function validateNonNegativeInteger(value, name) {
|
|
1384
|
+
if (!Number.isInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative integer`);
|
|
1385
|
+
}
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region src/create-node-converter.ts
|
|
1388
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
|
|
1389
|
+
const CORE_OPTION_KEYS = [
|
|
1390
|
+
"capabilities",
|
|
1391
|
+
"capabilityOverrides",
|
|
1392
|
+
"overflow",
|
|
1393
|
+
"strategy",
|
|
1394
|
+
"enableMath",
|
|
1395
|
+
"enableInlineMath",
|
|
1396
|
+
"enableFrontmatter",
|
|
1397
|
+
"mentionResolvers"
|
|
1398
|
+
];
|
|
1399
|
+
var ManagedNodeConverter = class {
|
|
1400
|
+
converter;
|
|
1401
|
+
runtime;
|
|
1402
|
+
defaultUploader;
|
|
1403
|
+
uploadScheduler;
|
|
1404
|
+
warmupOperation;
|
|
1405
|
+
activeScopes;
|
|
1406
|
+
activeReceipts;
|
|
1407
|
+
idleResolvers;
|
|
1408
|
+
activeOperations;
|
|
1409
|
+
closePromise;
|
|
1410
|
+
state;
|
|
1411
|
+
constructor(deps) {
|
|
1412
|
+
this.converter = deps.converter;
|
|
1413
|
+
this.runtime = deps.runtime;
|
|
1414
|
+
this.defaultUploader = deps.defaultUploader;
|
|
1415
|
+
this.uploadScheduler = deps.uploadScheduler;
|
|
1416
|
+
this.warmupOperation = deps.warmupOperation;
|
|
1417
|
+
this.activeScopes = /* @__PURE__ */ new Set();
|
|
1418
|
+
this.activeReceipts = /* @__PURE__ */ new Map();
|
|
1419
|
+
this.idleResolvers = [];
|
|
1420
|
+
this.activeOperations = 0;
|
|
1421
|
+
this.closePromise = void 0;
|
|
1422
|
+
this.state = NodeConverterState.Open;
|
|
1423
|
+
}
|
|
1424
|
+
convert(markdown, options = {}) {
|
|
1425
|
+
try {
|
|
1426
|
+
const normalized = normalizeNodeConvertOptions(options);
|
|
1427
|
+
return this.runTracked({
|
|
1428
|
+
signal: normalized.signal,
|
|
1429
|
+
timeoutMs: normalized.timeoutMs
|
|
1430
|
+
}, (operation, receipts) => this.converter.convertWithContext(markdown, prepareConversionOptions(normalized, this.defaultUploader, this.uploadScheduler), operation, receipts));
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
return Promise.reject(normalizeConfigurationError(error));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
convertToMessages(markdown, options = {}) {
|
|
1436
|
+
try {
|
|
1437
|
+
const normalized = normalizeNodeConvertOptions(options);
|
|
1438
|
+
return this.runTracked({
|
|
1439
|
+
signal: normalized.signal,
|
|
1440
|
+
timeoutMs: normalized.timeoutMs
|
|
1441
|
+
}, (operation, receipts) => this.converter.convertToMessagesWithContext(markdown, prepareConversionOptions(normalized, this.defaultUploader, this.uploadScheduler), operation, receipts));
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
return Promise.reject(normalizeConfigurationError(error));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
warmup(options = {}) {
|
|
1447
|
+
try {
|
|
1448
|
+
return this.runTracked(options, (operation) => this.warmupOperation(operation));
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
return Promise.reject(normalizeConfigurationError(error));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
close(options = {}) {
|
|
1454
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
1455
|
+
let behavior;
|
|
1456
|
+
let scope;
|
|
1457
|
+
try {
|
|
1458
|
+
validateCloseOptions(options);
|
|
1459
|
+
behavior = options.behavior ?? "drain";
|
|
1460
|
+
scope = createOperationScope({
|
|
1461
|
+
signal: options.signal,
|
|
1462
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS
|
|
1463
|
+
}, "close");
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
return Promise.reject(normalizeConfigurationError(error));
|
|
1466
|
+
}
|
|
1467
|
+
this.state = NodeConverterState.Closing;
|
|
1468
|
+
if (behavior === "cancel") for (const active of this.activeScopes) active.abortForShutdown("conversion");
|
|
1469
|
+
const closingReceipts = [...this.activeReceipts.values()];
|
|
1470
|
+
this.closePromise = this.finishClose(scope, behavior, closingReceipts);
|
|
1471
|
+
return this.closePromise;
|
|
1472
|
+
}
|
|
1473
|
+
status() {
|
|
1474
|
+
return Object.freeze({
|
|
1475
|
+
state: this.state,
|
|
1476
|
+
activeOperations: this.activeOperations,
|
|
1477
|
+
render: this.runtime.status(),
|
|
1478
|
+
upload: this.uploadScheduler.status()
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
[Symbol.asyncDispose]() {
|
|
1482
|
+
return this.close({
|
|
1483
|
+
behavior: "cancel",
|
|
1484
|
+
timeoutMs: DEFAULT_CLOSE_TIMEOUT_MS
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
runTracked(options, operation) {
|
|
1488
|
+
if (this.state !== NodeConverterState.Open) return Promise.reject(new OperationShutdownError$1("conversion_admission"));
|
|
1489
|
+
const scope = createOperationScope(options, "conversion");
|
|
1490
|
+
const receipts = new RecordingReceiptCollector();
|
|
1491
|
+
this.activeScopes.add(scope);
|
|
1492
|
+
this.activeReceipts.set(scope, receipts);
|
|
1493
|
+
this.activeOperations += 1;
|
|
1494
|
+
const underlying = Promise.resolve().then(() => operation(scope.context, receipts));
|
|
1495
|
+
underlying.then(() => {
|
|
1496
|
+
receipts.settle();
|
|
1497
|
+
this.finishTracked(scope);
|
|
1498
|
+
}, () => {
|
|
1499
|
+
receipts.settle();
|
|
1500
|
+
this.finishTracked(scope);
|
|
1501
|
+
});
|
|
1502
|
+
return scope.race(underlying).catch((error) => {
|
|
1503
|
+
throw lockReceiptSnapshot(attachReceipts(error, receipts.all(), receipts.settled()));
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
finishTracked(scope) {
|
|
1507
|
+
scope.cleanup();
|
|
1508
|
+
this.activeScopes.delete(scope);
|
|
1509
|
+
this.activeReceipts.delete(scope);
|
|
1510
|
+
this.activeOperations -= 1;
|
|
1511
|
+
if (this.activeOperations === 0) for (const resolve of this.idleResolvers.splice(0)) resolve();
|
|
1512
|
+
}
|
|
1513
|
+
async finishClose(scope, behavior, closingReceipts) {
|
|
1514
|
+
try {
|
|
1515
|
+
await scope.race(this.waitForIdle());
|
|
1516
|
+
await Promise.all([this.uploadScheduler.close(scope.context, behavior), this.runtime.close(scope.context, behavior)]);
|
|
1517
|
+
this.state = NodeConverterState.Closed;
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
this.state = NodeConverterState.CloseFailed;
|
|
1520
|
+
for (const active of this.activeScopes) active.abortForShutdown("conversion");
|
|
1521
|
+
const cleanupContext = {
|
|
1522
|
+
signal: void 0,
|
|
1523
|
+
deadlineMs: Date.now() + DEFAULT_CLOSE_TIMEOUT_MS
|
|
1524
|
+
};
|
|
1525
|
+
Promise.allSettled([this.uploadScheduler.close(cleanupContext, "cancel"), this.runtime.close(cleanupContext, "cancel")]);
|
|
1526
|
+
const receiptSnapshot = collectReceiptSnapshot(closingReceipts);
|
|
1527
|
+
const settledReceipts = collectSettledReceipts(closingReceipts);
|
|
1528
|
+
if (isOperationError(error) && error.code === OperationErrorCode$1.DeadlineExceeded) throw new NodeShutdownTimeoutError(this.status(), receiptSnapshot, settledReceipts);
|
|
1529
|
+
throw attachReceipts(error, receiptSnapshot, settledReceipts);
|
|
1530
|
+
} finally {
|
|
1531
|
+
scope.cleanup();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
waitForIdle() {
|
|
1535
|
+
if (this.activeOperations === 0) return Promise.resolve();
|
|
1536
|
+
return new Promise((resolve) => {
|
|
1537
|
+
this.idleResolvers.push(resolve);
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
function createNodeConverter(options = {}) {
|
|
1542
|
+
try {
|
|
1543
|
+
validateNodeFactoryOptions(options);
|
|
1544
|
+
const runtime = new PuppeteerRenderRuntime({
|
|
1545
|
+
launcher: new PuppeteerBrowserLauncher(),
|
|
1546
|
+
launchOptions: options.rendering?.launchOptions,
|
|
1547
|
+
concurrency: options.rendering?.concurrency,
|
|
1548
|
+
maxQueueSize: options.rendering?.maxQueueSize
|
|
1549
|
+
});
|
|
1550
|
+
const mermaidAssets = new LocalMermaidAssetLoader();
|
|
1551
|
+
const katexAssets = new LocalKatexAssetLoader();
|
|
1552
|
+
const builtInRenderers = [new MermaidRenderer({
|
|
1553
|
+
...options.rendering?.mermaid,
|
|
1554
|
+
runtime,
|
|
1555
|
+
assets: mermaidAssets
|
|
1556
|
+
}), new KatexRenderer({
|
|
1557
|
+
...options.rendering?.math,
|
|
1558
|
+
runtime,
|
|
1559
|
+
assets: katexAssets
|
|
1560
|
+
})];
|
|
1561
|
+
const additionalRenderers = Object.freeze([...options.additionalRenderers ?? []]);
|
|
1562
|
+
validateRendererIdentities(builtInRenderers, additionalRenderers);
|
|
1563
|
+
const converter = createConverter({
|
|
1564
|
+
...pickConversionDefaults(options),
|
|
1565
|
+
enableMath: options.enableMath ?? true,
|
|
1566
|
+
renderers: [...builtInRenderers, ...additionalRenderers]
|
|
1567
|
+
});
|
|
1568
|
+
const uploadScheduler = new UploadScheduler({
|
|
1569
|
+
concurrency: options.upload?.concurrency ?? 2,
|
|
1570
|
+
maxQueueSize: options.upload?.maxQueueSize ?? 16
|
|
1571
|
+
});
|
|
1572
|
+
return new ManagedNodeConverter({
|
|
1573
|
+
converter,
|
|
1574
|
+
runtime,
|
|
1575
|
+
defaultUploader: options.uploader,
|
|
1576
|
+
uploadScheduler,
|
|
1577
|
+
warmupOperation: (operation) => runtime.run(async () => {
|
|
1578
|
+
await Promise.all([
|
|
1579
|
+
mermaidAssets.loadScript(),
|
|
1580
|
+
katexAssets.loadScript(),
|
|
1581
|
+
katexAssets.loadCss()
|
|
1582
|
+
]);
|
|
1583
|
+
}, operation)
|
|
1584
|
+
});
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
if (isConfigurationError(error)) throw error;
|
|
1587
|
+
throw new ConfigurationError$1(`Invalid slackmark-node configuration${error instanceof Error ? `: ${error.message}` : ""}`, {
|
|
1588
|
+
code: "invalid_node_configuration",
|
|
1589
|
+
stage: "configuration",
|
|
1590
|
+
cause: error
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function prepareConversionOptions(options, defaultUploader, scheduler) {
|
|
1595
|
+
const { signal: _signal, timeoutMs: _timeoutMs, uploader: perCallUploader, ...conversionOptions } = options;
|
|
1596
|
+
const uploader = perCallUploader ?? defaultUploader;
|
|
1597
|
+
return {
|
|
1598
|
+
...conversionOptions,
|
|
1599
|
+
uploader: uploader === void 0 ? void 0 : new ScheduledSlackUploader(scheduler, uploader)
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function pickConversionDefaults(options) {
|
|
1603
|
+
return {
|
|
1604
|
+
capabilities: options.capabilities,
|
|
1605
|
+
capabilityOverrides: options.capabilityOverrides,
|
|
1606
|
+
overflow: options.overflow,
|
|
1607
|
+
strategy: options.strategy,
|
|
1608
|
+
enableMath: options.enableMath,
|
|
1609
|
+
enableInlineMath: options.enableInlineMath,
|
|
1610
|
+
enableFrontmatter: options.enableFrontmatter,
|
|
1611
|
+
mentionResolvers: options.mentionResolvers
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function validateNodeFactoryOptions(options) {
|
|
1615
|
+
validateKnownPlainObject(options, [
|
|
1616
|
+
...CORE_OPTION_KEYS,
|
|
1617
|
+
"rendering",
|
|
1618
|
+
"upload",
|
|
1619
|
+
"additionalRenderers",
|
|
1620
|
+
"uploader"
|
|
1621
|
+
], "options");
|
|
1622
|
+
if (options.rendering !== void 0) {
|
|
1623
|
+
validateKnownPlainObject(options.rendering, [
|
|
1624
|
+
"concurrency",
|
|
1625
|
+
"maxQueueSize",
|
|
1626
|
+
"launchOptions",
|
|
1627
|
+
"mermaid",
|
|
1628
|
+
"math"
|
|
1629
|
+
], "rendering");
|
|
1630
|
+
if (options.rendering.mermaid !== void 0) validateKnownPlainObject(options.rendering.mermaid, [
|
|
1631
|
+
"theme",
|
|
1632
|
+
"backgroundColor",
|
|
1633
|
+
"deviceScaleFactor",
|
|
1634
|
+
"maxSourceChars",
|
|
1635
|
+
"maxEdges",
|
|
1636
|
+
"maxOutputBytes",
|
|
1637
|
+
"maxWidth",
|
|
1638
|
+
"maxHeight"
|
|
1639
|
+
], "rendering.mermaid");
|
|
1640
|
+
if (options.rendering.math !== void 0) validateKnownPlainObject(options.rendering.math, [
|
|
1641
|
+
"backgroundColor",
|
|
1642
|
+
"deviceScaleFactor",
|
|
1643
|
+
"fontSizePx",
|
|
1644
|
+
"paddingPx",
|
|
1645
|
+
"maxExpand",
|
|
1646
|
+
"maxSize",
|
|
1647
|
+
"maxSourceChars",
|
|
1648
|
+
"maxOutputBytes",
|
|
1649
|
+
"maxWidth",
|
|
1650
|
+
"maxHeight"
|
|
1651
|
+
], "rendering.math");
|
|
1652
|
+
}
|
|
1653
|
+
if (options.upload !== void 0) validateKnownPlainObject(options.upload, ["concurrency", "maxQueueSize"], "upload");
|
|
1654
|
+
resolveCapabilities(options.capabilities, options.capabilityOverrides);
|
|
1655
|
+
if (options.enableMath !== void 0 && typeof options.enableMath !== "boolean") throw new ConfigurationError$1("enableMath must be boolean", {
|
|
1656
|
+
code: "invalid_option",
|
|
1657
|
+
stage: "configuration"
|
|
1658
|
+
});
|
|
1659
|
+
if (options.enableInlineMath !== void 0 && typeof options.enableInlineMath !== "boolean") throw new ConfigurationError$1("enableInlineMath must be boolean", {
|
|
1660
|
+
code: "invalid_option",
|
|
1661
|
+
stage: "configuration"
|
|
1662
|
+
});
|
|
1663
|
+
if (options.enableFrontmatter !== void 0 && typeof options.enableFrontmatter !== "boolean") throw new ConfigurationError$1("enableFrontmatter must be boolean", {
|
|
1664
|
+
code: "invalid_option",
|
|
1665
|
+
stage: "configuration"
|
|
1666
|
+
});
|
|
1667
|
+
if (options.overflow !== void 0 && options.overflow !== "degrade" && options.overflow !== "split") throw new ConfigurationError$1("overflow must be \"degrade\" or \"split\"", {
|
|
1668
|
+
code: "invalid_option",
|
|
1669
|
+
stage: "configuration"
|
|
1670
|
+
});
|
|
1671
|
+
if (options.strategy !== void 0 && options.strategy !== "rich-text-first" && options.strategy !== "markdown-block") throw new ConfigurationError$1("strategy must be \"rich-text-first\" or \"markdown-block\"", {
|
|
1672
|
+
code: "invalid_option",
|
|
1673
|
+
stage: "configuration"
|
|
1674
|
+
});
|
|
1675
|
+
validateMentionResolvers(options.mentionResolvers);
|
|
1676
|
+
if (options.additionalRenderers !== void 0 && !Array.isArray(options.additionalRenderers)) throw new ConfigurationError$1("additionalRenderers must be an array", {
|
|
1677
|
+
code: "invalid_renderer",
|
|
1678
|
+
stage: "configuration"
|
|
1679
|
+
});
|
|
1680
|
+
validateUploader(options.uploader);
|
|
1681
|
+
}
|
|
1682
|
+
function validateMentionResolvers(value) {
|
|
1683
|
+
if (value === void 0) return;
|
|
1684
|
+
validateKnownPlainObject(value, ["resolveUser", "resolveChannel"], "mentionResolvers");
|
|
1685
|
+
try {
|
|
1686
|
+
const resolveUser = Reflect.get(value, "resolveUser");
|
|
1687
|
+
const resolveChannel = Reflect.get(value, "resolveChannel");
|
|
1688
|
+
if (resolveUser !== void 0 && typeof resolveUser !== "function" || resolveChannel !== void 0 && typeof resolveChannel !== "function") throw new TypeError("mention resolvers must be functions");
|
|
1689
|
+
} catch (cause) {
|
|
1690
|
+
throw new ConfigurationError$1("mentionResolvers is invalid", {
|
|
1691
|
+
code: "invalid_option",
|
|
1692
|
+
stage: "configuration",
|
|
1693
|
+
cause
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function normalizeNodeConvertOptions(options) {
|
|
1698
|
+
const normalized = snapshotNodeOptions(options, [
|
|
1699
|
+
...CORE_OPTION_KEYS,
|
|
1700
|
+
"uploader",
|
|
1701
|
+
"signal",
|
|
1702
|
+
"timeoutMs"
|
|
1703
|
+
], "conversion options");
|
|
1704
|
+
validateUploader(normalized.uploader);
|
|
1705
|
+
return normalized;
|
|
1706
|
+
}
|
|
1707
|
+
function validateCloseOptions(options) {
|
|
1708
|
+
validateKnownPlainObject(options, [
|
|
1709
|
+
"behavior",
|
|
1710
|
+
"signal",
|
|
1711
|
+
"timeoutMs"
|
|
1712
|
+
], "close options");
|
|
1713
|
+
if (options.behavior !== void 0 && options.behavior !== "drain" && options.behavior !== "cancel") throw new ConfigurationError$1("close.behavior must be \"drain\" or \"cancel\"", {
|
|
1714
|
+
code: "invalid_close_behavior",
|
|
1715
|
+
stage: "configuration"
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
function validateKnownPlainObject(value, allowed, label) {
|
|
1719
|
+
if (!isPlainObject(value)) throw new ConfigurationError$1(`${label} must be a plain object`, {
|
|
1720
|
+
code: "invalid_option",
|
|
1721
|
+
stage: "configuration"
|
|
1722
|
+
});
|
|
1723
|
+
const allowedKeys = new Set(allowed);
|
|
1724
|
+
for (const key of Object.keys(value)) if (!allowedKeys.has(key)) throw new ConfigurationError$1(`Unknown ${label} option: ${key}`, {
|
|
1725
|
+
code: "invalid_option",
|
|
1726
|
+
stage: "configuration"
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
function validateUploader(uploader) {
|
|
1730
|
+
if (uploader === void 0) return;
|
|
1731
|
+
try {
|
|
1732
|
+
if (typeof uploader !== "object" && typeof uploader !== "function" || uploader === null || typeof Reflect.get(uploader, "upload") !== "function") throw new TypeError("upload must be a function");
|
|
1733
|
+
} catch (cause) {
|
|
1734
|
+
throw new ConfigurationError$1("uploader is invalid", {
|
|
1735
|
+
code: "invalid_uploader",
|
|
1736
|
+
stage: "configuration",
|
|
1737
|
+
cause
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function validateRendererIdentities(builtIns, additional) {
|
|
1742
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1743
|
+
for (const renderer of [...builtIns, ...additional]) {
|
|
1744
|
+
let id;
|
|
1745
|
+
let output;
|
|
1746
|
+
let canRender;
|
|
1747
|
+
let render;
|
|
1748
|
+
try {
|
|
1749
|
+
id = Reflect.get(renderer, "id");
|
|
1750
|
+
output = Reflect.get(renderer, "output");
|
|
1751
|
+
canRender = Reflect.get(renderer, "canRender");
|
|
1752
|
+
render = Reflect.get(renderer, "render");
|
|
1753
|
+
} catch (cause) {
|
|
1754
|
+
throw new ConfigurationError$1("Renderer id could not be read", {
|
|
1755
|
+
code: "invalid_renderer",
|
|
1756
|
+
stage: "configuration",
|
|
1757
|
+
cause
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (typeof id !== "string" || id.length === 0 || output !== "url" && output !== "bytes" && output !== "both" || typeof canRender !== "function" || typeof render !== "function") throw new ConfigurationError$1("Renderer descriptor is invalid", {
|
|
1761
|
+
code: "invalid_renderer",
|
|
1762
|
+
stage: "configuration"
|
|
1763
|
+
});
|
|
1764
|
+
if (ids.has(id)) throw new ConfigurationError$1(`Duplicate renderer id: ${id}`, {
|
|
1765
|
+
code: "duplicate_renderer_id",
|
|
1766
|
+
stage: "configuration"
|
|
1767
|
+
});
|
|
1768
|
+
ids.add(id);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function collectReceiptSnapshot(collectors) {
|
|
1772
|
+
return Object.freeze(collectors.flatMap((collector) => collector.all()));
|
|
1773
|
+
}
|
|
1774
|
+
async function collectSettledReceipts(collectors) {
|
|
1775
|
+
try {
|
|
1776
|
+
const settled = await Promise.all(collectors.map((collector) => collector.settled()));
|
|
1777
|
+
return Object.freeze(settled.flat());
|
|
1778
|
+
} catch {
|
|
1779
|
+
return collectReceiptSnapshot(collectors);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function normalizeConfigurationError(error) {
|
|
1783
|
+
return isConfigurationError(error) ? error : new ConfigurationError$1("Node converter options could not be read", {
|
|
1784
|
+
code: "invalid_option",
|
|
1785
|
+
stage: "configuration",
|
|
1786
|
+
cause: error
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
//#endregion
|
|
1790
|
+
export { ConfigurationError, NodeConverterState, NodeRenderError, NodeRenderErrorCode, NodeRenderStage, NodeShutdownTimeoutError, OperationAbortedError, OperationDeadlineExceededError, OperationError, OperationErrorCode, OperationShutdownError, createNodeConverter };
|
|
1791
|
+
|
|
1792
|
+
//# sourceMappingURL=index.js.map
|