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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/THIRD_PARTY_NOTICES.md +14 -0
  4. package/dist/create-node-converter.d.ts +72 -0
  5. package/dist/create-node-converter.d.ts.map +1 -0
  6. package/dist/errors.d.ts +60 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/image-output.d.ts +9 -0
  9. package/dist/image-output.d.ts.map +1 -0
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +1792 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/katex-assets.d.ts +19 -0
  15. package/dist/katex-assets.d.ts.map +1 -0
  16. package/dist/katex-renderer.d.ts +39 -0
  17. package/dist/katex-renderer.d.ts.map +1 -0
  18. package/dist/mermaid-assets.d.ts +9 -0
  19. package/dist/mermaid-assets.d.ts.map +1 -0
  20. package/dist/mermaid-renderer.d.ts +33 -0
  21. package/dist/mermaid-renderer.d.ts.map +1 -0
  22. package/dist/node-options.d.ts +6 -0
  23. package/dist/node-options.d.ts.map +1 -0
  24. package/dist/operation-control.d.ts +18 -0
  25. package/dist/operation-control.d.ts.map +1 -0
  26. package/dist/puppeteer-render-runtime.d.ts +76 -0
  27. package/dist/puppeteer-render-runtime.d.ts.map +1 -0
  28. package/dist/puppeteer.d.ts +7 -0
  29. package/dist/puppeteer.d.ts.map +1 -0
  30. package/dist/puppeteer.js +723 -0
  31. package/dist/puppeteer.js.map +1 -0
  32. package/dist/renderers.d.ts +5 -0
  33. package/dist/renderers.d.ts.map +1 -0
  34. package/dist/renderers.js +551 -0
  35. package/dist/renderers.js.map +1 -0
  36. package/dist/status.d.ts +20 -0
  37. package/dist/status.d.ts.map +1 -0
  38. package/dist/upload-scheduler.d.ts +32 -0
  39. package/dist/upload-scheduler.d.ts.map +1 -0
  40. 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