satoru-render 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,9 +103,9 @@ const png = await render({
103
103
 
104
104
  ## 🛠️ Advanced Usage
105
105
 
106
- ### 1. Dynamic Resource Resolution
106
+ ### 1. Dynamic Resource Resolution & Caching
107
107
 
108
- Satoru can automatically fetch missing fonts, images, or external CSS via a `resolveResource` callback.
108
+ Satoru can automatically fetch missing fonts, images, or external CSS via a `resolveResource` callback. You can also implement high-performance caching using the browser's `CacheStorage` API.
109
109
 
110
110
  ```typescript
111
111
  const pdf = await render({
@@ -114,12 +114,23 @@ const pdf = await render({
114
114
  format: "pdf",
115
115
  baseUrl: "https://example.com/assets/",
116
116
  resolveResource: async (resource, defaultResolver) => {
117
- // Custom intercept logic
118
- if (resource.url.startsWith("my-app://")) {
119
- return myAssetBuffer;
117
+ // 1. Open Cache storage
118
+ const cache = await caches.open("satoru-resource-cache");
119
+ const cachedResponse = await cache.match(resource.url);
120
+
121
+ // 2. Return cached data if available
122
+ if (cachedResponse) {
123
+ const buf = await cachedResponse.arrayBuffer();
124
+ return new Uint8Array(buf);
120
125
  }
121
- // Fallback to default fetch/filesystem resolver
122
- return defaultResolver(resource);
126
+
127
+ // 3. Fetch using default resolver and save to cache
128
+ const data = await defaultResolver(resource);
129
+ if (data?.length) {
130
+ await cache.put(resource.url, new Response(data));
131
+ }
132
+
133
+ return data;
123
134
  },
124
135
  });
125
136
  ```
@@ -220,17 +231,81 @@ const png = await render({
220
231
 
221
232
  ---
222
233
 
234
+ ### 6. JSDOM Hydration (For Next.js / SPAs)
235
+
236
+ For complex client-side applications (like Next.js) that require full Javascript evaluation and DOM hydration before rendering, Satoru provides an optional `jsdom` helper.
237
+
238
+ _Note: `jsdom` must be installed separately in your project (`npm install jsdom`)._
239
+
240
+ ```typescript
241
+ import { render } from "satoru-render";
242
+ import { getHtml } from "satoru-render/jsdom";
243
+
244
+ // 1. Let JSDOM fetch the URL, execute scripts, and wait for network/hydration
245
+ const hydratedHtml = await getHtml({
246
+ src: "https://example.com/",
247
+ waitUntil: "networkidle", // Wait until Next.js finishes loading chunks
248
+ beforeParse: (window) => {
249
+ // Provide polyfills if the target site requires them
250
+ window.matchMedia = () => ({ matches: false, addListener: () => {} });
251
+ window.IntersectionObserver = class {
252
+ observe() {}
253
+ unobserve() {}
254
+ disconnect() {}
255
+ };
256
+ },
257
+ });
258
+
259
+ // 2. Render the fully constructed DOM in Satoru (at native speed)
260
+ const pngBytes = await render({
261
+ value: hydratedHtml,
262
+ baseUrl: "https://example.com/",
263
+ width: 1200,
264
+ format: "png",
265
+ });
266
+ ```
267
+
268
+ ### 7. DOM Capture (html2canvas alternative)
269
+
270
+ Satoru can capture live DOM elements directly in the browser, preserving computed styles, pseudo-elements (`::before`/`::after`), canvas contents, and form states.
271
+
272
+ ```typescript
273
+ import { Satoru } from "satoru-render";
274
+ import createSatoruModule from "satoru-render/satoru.js";
275
+
276
+ const satoru = await Satoru.create(createSatoruModule);
277
+ const element = document.getElementById("target-element");
278
+
279
+ // 1. Capture to HTMLCanvasElement (Direct html2canvas replacement)
280
+ const canvas = await satoru.capture(element, {
281
+ format: "png",
282
+ });
283
+ document.body.appendChild(canvas);
284
+
285
+ // 2. Or render directly to binary (Uint8Array)
286
+ const png = await satoru.render({
287
+ value: element, // Accepts HTMLElement directly
288
+ width: 800,
289
+ format: "png",
290
+ });
291
+ ```
292
+
293
+ ---
294
+
223
295
  ## 💻 CLI Tool
224
296
 
225
297
  Convert files or URLs directly from your terminal.
226
298
 
227
299
  ```bash
228
- # Local HTML to PNG
300
+ # Local HTML to PNG (JSDOM hydration enabled by default)
229
301
  npx satoru-render input.html -o output.png
230
302
 
231
303
  # URL to PDF with specific width
232
304
  npx satoru-render https://example.com -o site.pdf -w 1280
233
305
 
306
+ # Convert without JSDOM hydration
307
+ npx satoru-render https://example.com --no-jsdom -o example.pdf
308
+
234
309
  # WebP conversion with verbose logs
235
310
  npx satoru-render input.html -f webp --verbose
236
311
  ```
@@ -241,9 +316,9 @@ npx satoru-render input.html -f webp --verbose
241
316
 
242
317
  ### Render Options
243
318
 
244
- | Option | Type | Description |
245
- | :---------------- | :---------------------------------- | :------------------------------------------------------ |
246
- | `value` | `string \| string[]` | HTML string or array of strings (for multi-page PDF). |
319
+ | Option | Type | Description |
320
+ | :---------------- | :--------------------------------------- | :------------------------------------------------------ |
321
+ | `value` | `string \| string[] \| HTMLElement \| ...` | HTML string, array of strings, or DOM element(s). |
247
322
  | `url` | `string` | URL to fetch HTML from. |
248
323
  | `width` | `number` | **Required.** Output width in pixels. |
249
324
  | `height` | `number` | Output height. Default: `0` (auto-calculate). |
@@ -0,0 +1 @@
1
+ export {};
package/dist/bench.js ADDED
@@ -0,0 +1,70 @@
1
+ import { Bench } from "tinybench";
2
+ import { Satoru } from "./node.js";
3
+ // @ts-ignore
4
+ import createSatoruModule from "../dist/satoru-single.js";
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { fileURLToPath } from "url";
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const assetsDir = path.join(__dirname, "../../../assets");
10
+ async function runBenchmark() {
11
+ const satoru = await Satoru.create(createSatoruModule);
12
+ const bench = new Bench({ time: 1000 });
13
+ const files = [
14
+ "06-flexbox.html",
15
+ "09-grid.html",
16
+ "11-complex-text.html",
17
+ "15-showcase.html",
18
+ ];
19
+ for (const file of files) {
20
+ const filePath = path.join(assetsDir, file);
21
+ if (!fs.existsSync(filePath)) {
22
+ console.warn(`File not found: ${filePath}`);
23
+ continue;
24
+ }
25
+ const html = fs.readFileSync(filePath, "utf-8");
26
+ // Full render
27
+ bench.add(`Full Render: ${file}`, async () => {
28
+ await satoru.render({
29
+ value: html,
30
+ width: 1200,
31
+ format: "svg",
32
+ baseUrl: `file://${assetsDir}/`,
33
+ });
34
+ });
35
+ // Core layout and render (no resource discovery if we assume resources are loaded)
36
+ // To make it fair, we'll initialize once and layout/render multiple times
37
+ // However, tinybench runs the function multiple times, so we need to be careful about state.
38
+ // For core, we'll do init inside to avoid state pollution, but the focus is on layout/render.
39
+ bench.add(`Core Layout+Render (SVG): ${file}`, async () => {
40
+ const inst = await satoru.initDocument({
41
+ html,
42
+ width: 1200,
43
+ baseUrl: `file://${assetsDir}/`,
44
+ });
45
+ await satoru.layoutDocument(inst, 1200);
46
+ await satoru.renderFromState(inst, {
47
+ width: 1200,
48
+ format: "svg",
49
+ });
50
+ await satoru.destroyInstance(inst);
51
+ });
52
+ bench.add(`Core Layout+Render (PNG): ${file}`, async () => {
53
+ const inst = await satoru.initDocument({
54
+ html,
55
+ width: 1200,
56
+ baseUrl: `file://${assetsDir}/`,
57
+ });
58
+ await satoru.layoutDocument(inst, 1200);
59
+ await satoru.renderFromState(inst, {
60
+ width: 1200,
61
+ format: "png",
62
+ });
63
+ await satoru.destroyInstance(inst);
64
+ });
65
+ }
66
+ console.log("Running benchmarks...");
67
+ await bench.run();
68
+ console.table(bench.table());
69
+ }
70
+ runBenchmark().catch(console.error);
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ async function main() {
7
7
  const options = {
8
8
  width: 800,
9
9
  format: "png",
10
+ jsdom: true, // Default to true
10
11
  };
11
12
  let input;
12
13
  for (let i = 0; i < args.length; i++) {
@@ -23,6 +24,9 @@ async function main() {
23
24
  else if (arg === "-o" || arg === "--output") {
24
25
  options.output = args[++i];
25
26
  }
27
+ else if (arg === "--no-jsdom") {
28
+ options.jsdom = false;
29
+ }
26
30
  else if (arg === "--verbose") {
27
31
  options.verbose = true;
28
32
  }
@@ -70,16 +74,63 @@ async function main() {
70
74
  console.error(`[Satoru] ${LogLevel[level]}: ${message}`);
71
75
  };
72
76
  }
77
+ let finalHtml;
78
+ let baseUrl;
73
79
  if (isUrl) {
74
- renderOptions.url = input;
80
+ baseUrl = input;
81
+ if (options.jsdom) {
82
+ try {
83
+ const { getHtml } = await import("./jsdom.js");
84
+ if (options.verbose)
85
+ console.error(`[Satoru] Hydrating URL via JSDOM: ${input}`);
86
+ finalHtml = await getHtml({
87
+ src: input,
88
+ waitUntil: "networkidle",
89
+ forwardConsole: options.verbose,
90
+ });
91
+ }
92
+ catch (err) {
93
+ if (options.verbose)
94
+ console.error(`[Satoru] JSDOM hydration failed or not available, falling back to direct URL fetch:`, err);
95
+ renderOptions.url = input;
96
+ }
97
+ }
98
+ else {
99
+ renderOptions.url = input;
100
+ }
75
101
  }
76
102
  else {
77
103
  if (!fs.existsSync(input)) {
78
104
  console.error(`Error: File not found: ${input}`);
79
105
  process.exit(1);
80
106
  }
81
- renderOptions.value = fs.readFileSync(input, "utf-8");
82
- renderOptions.baseUrl = path.dirname(path.resolve(input));
107
+ const rawContent = fs.readFileSync(input, "utf-8");
108
+ baseUrl = path.dirname(path.resolve(input));
109
+ if (options.jsdom) {
110
+ try {
111
+ const { getHtml } = await import("./jsdom.js");
112
+ if (options.verbose)
113
+ console.error(`[Satoru] Hydrating file via JSDOM: ${input}`);
114
+ finalHtml = await getHtml({
115
+ src: rawContent,
116
+ baseUrl: `file://${baseUrl}/`,
117
+ waitUntil: "networkidle",
118
+ forwardConsole: options.verbose,
119
+ });
120
+ }
121
+ catch (err) {
122
+ if (options.verbose)
123
+ console.error(`[Satoru] JSDOM hydration failed or not available, falling back to raw file content:`, err);
124
+ finalHtml = rawContent;
125
+ }
126
+ }
127
+ else {
128
+ finalHtml = rawContent;
129
+ }
130
+ }
131
+ if (finalHtml !== undefined) {
132
+ renderOptions.value = finalHtml;
133
+ renderOptions.baseUrl = baseUrl;
83
134
  }
84
135
  try {
85
136
  const result = await satoru.render(renderOptions);
@@ -100,6 +151,7 @@ Options:
100
151
  -w, --width <number> Viewport width (default: 800)
101
152
  -h, --height <number> Viewport height (default: 0, auto-calculate)
102
153
  -f, --format <format> Output format: svg, png, webp, pdf
154
+ --no-jsdom Disable JSDOM hydration (enabled by default)
103
155
  --verbose Enable detailed logging
104
156
  --help Show this help message
105
157
  `);
package/dist/core.d.ts CHANGED
@@ -7,7 +7,9 @@ export interface SatoruModule {
7
7
  add_resource: (inst: any, url: string, type: number, data: Uint8Array) => void;
8
8
  scan_css: (inst: any, css: string) => void;
9
9
  load_font: (inst: any, name: string, data: Uint8Array) => void;
10
+ load_fallback_font: (inst: any, data: Uint8Array) => void;
10
11
  load_image: (inst: any, name: string, url: string, width: number, height: number) => void;
12
+ load_image_pixels: (inst: any, name: string, width: number, height: number, pixels: Uint8Array, data_url: string) => void;
11
13
  set_font_map: (inst: any, fontMap: Record<string, string>) => void;
12
14
  set_log_level: (level: number) => void;
13
15
  init_document: (inst: any, html: string, width: number, height: number) => void;
@@ -26,7 +28,7 @@ export interface RequiredResource {
26
28
  }
27
29
  export type ResourceResolver = (resource: RequiredResource, defaultResolver: (resource: RequiredResource) => Promise<Uint8Array | null>) => Promise<Uint8Array | ArrayBufferView | null>;
28
30
  export interface RenderOptions {
29
- value?: string | string[];
31
+ value?: string | string[] | any | any[];
30
32
  url?: string;
31
33
  width: number;
32
34
  height?: number;
@@ -37,6 +39,7 @@ export interface RenderOptions {
37
39
  name: string;
38
40
  data: Uint8Array;
39
41
  }[];
42
+ fallbackFonts?: Uint8Array[];
40
43
  images?: {
41
44
  name: string;
42
45
  url: string;
@@ -69,6 +72,7 @@ export declare abstract class SatoruBase {
69
72
  textToPaths?: boolean;
70
73
  }): Promise<string | Uint8Array>;
71
74
  destroyInstance(inst: any): Promise<void>;
75
+ loadFallbackFont(data: Uint8Array): Promise<void>;
72
76
  protected abstract resolveDefaultResource(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
73
77
  protected abstract fetchHtml(url: string, userAgent?: string): Promise<string>;
74
78
  render(options: RenderOptions & {
package/dist/core.js CHANGED
@@ -150,6 +150,16 @@ export class SatoruBase {
150
150
  const mod = await this.getModule();
151
151
  mod.destroy_instance(inst);
152
152
  }
153
+ async loadFallbackFont(data) {
154
+ const mod = await this.getModule();
155
+ const inst = mod.create_instance();
156
+ try {
157
+ mod.load_fallback_font(inst, data);
158
+ }
159
+ finally {
160
+ mod.destroy_instance(inst);
161
+ }
162
+ }
153
163
  async render(options) {
154
164
  const mod = await this.getModule();
155
165
  let { value, url, width, height = 0, format = "svg", fonts, images, css, baseUrl, logLevel, onLog, } = options;
@@ -181,6 +191,11 @@ export class SatoruBase {
181
191
  mod.load_font(instancePtr, f.name, f.data);
182
192
  }
183
193
  }
194
+ if (options.fallbackFonts) {
195
+ for (const data of options.fallbackFonts) {
196
+ mod.load_fallback_font(instancePtr, data);
197
+ }
198
+ }
184
199
  if (images) {
185
200
  for (const img of images) {
186
201
  mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
@@ -225,6 +240,23 @@ export class SatoruBase {
225
240
  const uint8 = data instanceof Uint8Array
226
241
  ? data
227
242
  : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
243
+ if (r.type === "image" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") {
244
+ try {
245
+ const blob = new Blob([uint8.buffer]);
246
+ const bitmap = await createImageBitmap(blob);
247
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
248
+ const ctx = canvas.getContext("2d");
249
+ if (ctx) {
250
+ ctx.drawImage(bitmap, 0, 0);
251
+ const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
252
+ mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
253
+ return;
254
+ }
255
+ }
256
+ catch (e) {
257
+ // fallback to passing raw binary if decoding fails
258
+ }
259
+ }
228
260
  let typeInt = 1;
229
261
  if (r.type === "image")
230
262
  typeInt = 2;
@@ -247,7 +279,7 @@ export class SatoruBase {
247
279
  });
248
280
  resolvedUrls.forEach((url) => {
249
281
  const escapedUrl = url.replace(/[.*+?^${}()|[\]]/g, "\\$&");
250
- const linkRegex = new RegExp(`<link[^>]*href\\s*=\\s*(["'])${escapedUrl}\\1[^>]*>`, "gi");
282
+ const linkRegex = new RegExp(`<link[^>]*href\\s*=\\s*(["']?)${escapedUrl}\\1[^>]*>`, "gi");
251
283
  processedHtml = processedHtml.replace(linkRegex, "");
252
284
  });
253
285
  processedHtmls.push(processedHtml);
@@ -267,7 +299,7 @@ export class SatoruBase {
267
299
  if (format === "svg") {
268
300
  return new TextDecoder().decode(result);
269
301
  }
270
- return new Uint8Array(result);
302
+ return new Uint8Array(result.slice());
271
303
  }
272
304
  finally {
273
305
  mod.destroy_instance(instancePtr);
package/dist/index.d.ts CHANGED
@@ -1,9 +1,26 @@
1
- import { SatoruBase, RequiredResource } from "./core.js";
1
+ import { SatoruBase, RequiredResource, RenderOptions } from "./core.js";
2
2
  export * from "./core.js";
3
3
  export * from "./log-level.js";
4
4
  export type { SatoruWorker } from "./child-workers.js";
5
5
  export declare class Satoru extends SatoruBase {
6
6
  static create(createSatoruModuleFunc: any): Promise<Satoru>;
7
+ /**
8
+ * Captures an HTMLElement and returns an HTMLCanvasElement.
9
+ * Similar to html2canvas.
10
+ */
11
+ capture(element: HTMLElement, options?: Partial<Omit<RenderOptions, "value" | "url">>): Promise<HTMLCanvasElement>;
12
+ private serializeElement;
13
+ private pngToCanvas;
14
+ /**
15
+ * Overrides render to support HTMLElement as value.
16
+ */
17
+ render(options: RenderOptions & {
18
+ format: "png" | "webp" | "pdf";
19
+ }): Promise<Uint8Array>;
20
+ render(options: RenderOptions & {
21
+ format?: "svg";
22
+ }): Promise<string>;
23
+ render(options: RenderOptions): Promise<string | Uint8Array>;
7
24
  static defaultResourceResolver(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
8
25
  protected resolveDefaultResource(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
9
26
  protected fetchHtml(url: string, userAgent?: string): Promise<string>;
package/dist/index.js CHANGED
@@ -5,6 +5,169 @@ export class Satoru extends SatoruBase {
5
5
  static async create(createSatoruModuleFunc) {
6
6
  return new Satoru(createSatoruModuleFunc);
7
7
  }
8
+ /**
9
+ * Captures an HTMLElement and returns an HTMLCanvasElement.
10
+ * Similar to html2canvas.
11
+ */
12
+ async capture(element, options = {}) {
13
+ const rect = element.getBoundingClientRect();
14
+ const width = options.width ?? Math.ceil(rect.width);
15
+ const height = options.height ?? Math.ceil(rect.height);
16
+ // 1. Serialize DOM with computed styles
17
+ const serializedHtml = this.serializeElement(element);
18
+ // 2. Render to PNG
19
+ const pngData = await this.render({
20
+ ...options,
21
+ value: serializedHtml,
22
+ width,
23
+ height,
24
+ format: "png",
25
+ });
26
+ // 3. Convert PNG to Canvas
27
+ return this.pngToCanvas(pngData, width, height);
28
+ }
29
+ serializeElement(element) {
30
+ const clone = element.cloneNode(true);
31
+ const absoluteUrl = (url) => {
32
+ if (!url || url.startsWith("data:") || url.startsWith("blob:"))
33
+ return url;
34
+ const a = document.createElement("a");
35
+ a.href = url;
36
+ return a.href;
37
+ };
38
+ const applyStyles = (src, dest) => {
39
+ const computed = window.getComputedStyle(src);
40
+ const styleArr = [];
41
+ for (let i = 0; i < computed.length; i++) {
42
+ const prop = computed[i];
43
+ let value = computed.getPropertyValue(prop);
44
+ // Convert relative URLs in styles (e.g., background-image)
45
+ if (value.includes("url(")) {
46
+ value = value.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, url) => {
47
+ return `url("${absoluteUrl(url)}")`;
48
+ });
49
+ }
50
+ styleArr.push(`${prop}:${value}`);
51
+ }
52
+ dest.setAttribute("style", styleArr.join(";"));
53
+ // Handle pseudo-elements
54
+ const handlePseudo = (pseudoType) => {
55
+ const style = window.getComputedStyle(src, pseudoType);
56
+ const content = style.getPropertyValue("content");
57
+ if (!content || content === "none" || content === "normal")
58
+ return;
59
+ const pseudoEl = document.createElement("span");
60
+ const pseudoStyles = [];
61
+ for (let i = 0; i < style.length; i++) {
62
+ const prop = style[i];
63
+ pseudoStyles.push(`${prop}:${style.getPropertyValue(prop)}`);
64
+ }
65
+ pseudoEl.setAttribute("style", pseudoStyles.join(";"));
66
+ // Remove quotes from content for the innerText if it's just a string
67
+ if (content.startsWith('"') && content.endsWith('"')) {
68
+ pseudoEl.innerText = content.slice(1, -1);
69
+ }
70
+ if (pseudoType === "::before") {
71
+ dest.prepend(pseudoEl);
72
+ }
73
+ else {
74
+ dest.append(pseudoEl);
75
+ }
76
+ };
77
+ handlePseudo("::before");
78
+ handlePseudo("::after");
79
+ // Special handling for specific elements
80
+ if (src instanceof HTMLCanvasElement) {
81
+ const img = document.createElement("img");
82
+ img.src = src.toDataURL();
83
+ img.setAttribute("style", dest.getAttribute("style") || "");
84
+ dest.replaceWith(img);
85
+ }
86
+ else if (src instanceof HTMLImageElement) {
87
+ dest.setAttribute("src", absoluteUrl(src.src));
88
+ }
89
+ else if (src instanceof HTMLInputElement) {
90
+ if (src.type === "checkbox" || src.type === "radio") {
91
+ if (src.checked)
92
+ dest.setAttribute("checked", "");
93
+ }
94
+ else {
95
+ dest.setAttribute("value", src.value);
96
+ }
97
+ }
98
+ else if (src instanceof HTMLTextAreaElement) {
99
+ dest.innerText = src.value;
100
+ }
101
+ else if (src instanceof HTMLSelectElement) {
102
+ // Find the selected option and mark it
103
+ const index = src.selectedIndex;
104
+ const clonedSelect = dest;
105
+ if (clonedSelect.options[index]) {
106
+ clonedSelect.options[index].setAttribute("selected", "");
107
+ }
108
+ }
109
+ for (let i = 0; i < src.children.length; i++) {
110
+ const srcChild = src.children[i];
111
+ const destChild = dest.children[i];
112
+ if (srcChild && destChild) {
113
+ applyStyles(srcChild, destChild);
114
+ }
115
+ }
116
+ };
117
+ applyStyles(element, clone);
118
+ return `<!DOCTYPE html><html><body style="margin:0;padding:0;overflow:hidden;background:transparent;">${clone.outerHTML}</body></html>`;
119
+ }
120
+ async pngToCanvas(data, width, height) {
121
+ const blob = new Blob([data], { type: "image/png" });
122
+ const url = URL.createObjectURL(blob);
123
+ try {
124
+ const img = new Image();
125
+ img.crossOrigin = "anonymous";
126
+ await new Promise((resolve, reject) => {
127
+ img.onload = resolve;
128
+ img.onerror = reject;
129
+ img.src = url;
130
+ });
131
+ const canvas = document.createElement("canvas");
132
+ canvas.width = width;
133
+ canvas.height = height;
134
+ const ctx = canvas.getContext("2d");
135
+ if (ctx) {
136
+ ctx.drawImage(img, 0, 0);
137
+ }
138
+ return canvas;
139
+ }
140
+ finally {
141
+ URL.revokeObjectURL(url);
142
+ }
143
+ }
144
+ async render(options) {
145
+ if (options.value) {
146
+ const values = Array.isArray(options.value)
147
+ ? options.value
148
+ : [options.value];
149
+ const processedValues = [];
150
+ let hasElement = false;
151
+ for (const val of values) {
152
+ if (val instanceof HTMLElement) {
153
+ processedValues.push(this.serializeElement(val));
154
+ hasElement = true;
155
+ }
156
+ else {
157
+ processedValues.push(val);
158
+ }
159
+ }
160
+ if (hasElement) {
161
+ options = {
162
+ ...options,
163
+ value: Array.isArray(options.value)
164
+ ? processedValues
165
+ : processedValues[0],
166
+ };
167
+ }
168
+ }
169
+ return super.render(options);
170
+ }
8
171
  static async defaultResourceResolver(resource, baseUrl, userAgent) {
9
172
  try {
10
173
  if (resource.url.startsWith("provider:google-fonts")) {
@@ -0,0 +1,41 @@
1
+ export interface HydrateOptions {
2
+ /**
3
+ * Target URL or raw HTML string to render.
4
+ */
5
+ src: string;
6
+ /**
7
+ * Base URL for resolving relative paths and fetching resources.
8
+ * Recommended when passing raw HTML.
9
+ */
10
+ baseUrl?: string;
11
+ /**
12
+ * Wait condition for hydration to complete.
13
+ * - number: Wait for specified milliseconds
14
+ * - function: Polling function that receives the window object and returns true when done
15
+ * - "networkidle": Waits until network requests settle (basic implementation)
16
+ */
17
+ waitUntil?: number | ((window: any) => boolean | Promise<boolean>) | "networkidle";
18
+ /**
19
+ * Maximum time to wait in milliseconds. Default: 10000ms.
20
+ */
21
+ timeout?: number;
22
+ /**
23
+ * Callback to mock or inject missing browser APIs (e.g. matchMedia, IntersectionObserver)
24
+ * before the HTML is parsed and scripts are executed.
25
+ */
26
+ beforeParse?: (window: any) => void | Promise<void>;
27
+ /**
28
+ * If true, forwards console.log/error from JSDOM to the Node.js console.
29
+ */
30
+ forwardConsole?: boolean;
31
+ /**
32
+ * If true, removes all <script> tags from the final HTML before returning.
33
+ * Default: true.
34
+ */
35
+ removeScripts?: boolean;
36
+ }
37
+ /**
38
+ * Hydrates an HTML string or URL using JSDOM and returns the final rendered HTML.
39
+ * Note: Requires 'jsdom' to be installed as a peer dependency.
40
+ */
41
+ export declare function getHtml(options: HydrateOptions): Promise<string>;