@zumer/snapdom 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ZumerLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # snapDOM
2
+
3
+ **snapDOM** is a high-fidelity DOM capture tool, developed as part of the animation engine for Zumly — a framework for creating smooth zoom-based view transitions.
4
+
5
+ It converts any HTML element into a scalable SVG image, preserving styles, fonts, backgrounds, shadow DOM content, pseudo-elements, and more.
6
+
7
+ - 📸 Full DOM capture
8
+ - 🎨 Embedded styles, pseudo-elements, and fonts
9
+ - 🖼️ Export to SVG, PNG, JPG, WebP, or `canvas`
10
+ - ⚡ Lightweight, no dependencies
11
+ - 📦 100% based on standard Web APIs
12
+
13
+ ---
14
+ ¡Obvio, Martín! 🔥
15
+ Acá te integro la parte de **Installation** agregando también la opción **CDN**, de manera prolija y siguiendo tu estilo:
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ You can use **snapDOM** by including it via **CDN**, **script tag**, or by **importing it as a module**.
22
+
23
+ ### CDN
24
+
25
+ ```html
26
+ <script src="https://unpkg.com/@zumer/snapdom@latest/snapdom.min.js"></script>
27
+ ```
28
+
29
+ ---
30
+
31
+ ### Script tag (local)
32
+
33
+ ```html
34
+ <script src="snapdom.js"></script>
35
+ ```
36
+
37
+ The global object `snapdom` will be available.
38
+
39
+ ---
40
+
41
+ ### ES Module
42
+
43
+ ```javascript
44
+ import { snapdom } from './snapdom.mjs';
45
+ ```
46
+
47
+ ### Script Tag (Type Module)
48
+
49
+ ```
50
+ <script type="module">
51
+ import { snapdom } from 'https://unpkg.com/@zumer/snapdom@latest/snapdom.mjs';
52
+ </script>
53
+ ```
54
+
55
+ Now you can call `snapdom(el)`, `snapdom.toPng(el)`, etc., directly in your JavaScript.
56
+
57
+ ---
58
+
59
+ ## Basic usage
60
+
61
+ ```javascript
62
+ // Capture an element as SVG Data URL
63
+ const svgDataUrl = await snapdom(document.querySelector("#myElement"));
64
+
65
+ // Insert the captured image into the page
66
+ const img = new Image();
67
+ img.src = svgDataUrl;
68
+ document.body.appendChild(img);
69
+ ```
70
+
71
+ ---
72
+
73
+ ## API
74
+
75
+ The main API is exposed as `snapdom` and offers multiple capture methods:
76
+
77
+ | Method | Description | Returns |
78
+ |:-------|:------------|:--------|
79
+ | `snapdom(el, scale?)` | Captures as SVG Data URL | `Promise<string>` |
80
+ | `snapdom.toImg(el, scale?)` | Captures as `HTMLImageElement` (SVG) | `Promise<HTMLImageElement>` |
81
+ | `snapdom.toCanvas(el, scale?)` | Captures as `HTMLCanvasElement` | `Promise<HTMLCanvasElement>` |
82
+ | `snapdom.toPng(el, scale?)` | Captures as PNG image (`Image`) | `Promise<HTMLImageElement>` |
83
+ | `snapdom.toJpg(el, scale?, quality?)` | Captures as JPG image (`Image`) | `Promise<HTMLImageElement>` |
84
+ | `snapdom.toWebp(el, scale?, quality?)` | Captures as WebP image (`Image`) | `Promise<HTMLImageElement>` |
85
+ | `snapdom.toBlob(el, scale?)` | Captures as SVG `Blob` | `Promise<Blob>` |
86
+
87
+ **Parameters:**
88
+ - `el`: DOM element to capture.
89
+ - `scale`: Scale factor (default is `1`).
90
+ - `quality`: Compression quality for JPG/WebP (range `0`–`1`).
91
+
92
+ ---
93
+
94
+ ## Special features
95
+
96
+ - **Shadow DOM**: Captures content inside Web Components and `shadowRoot`.
97
+ - **Pseudo-elements**: Captures `::before` and `::after`, including background images.
98
+ - **Backgrounds and images**: Inlines external images as Data URLs.
99
+ - **Fonts**: Replicates applied font families without needing external font files.
100
+ - **Placeholder and Exclusion**:
101
+ - `data-capture="exclude"`: Skips an element while preserving layout space.
102
+ - `data-capture="placeholder"` + `data-placeholder-text="Text"`: Replaces an element with decorative placeholder text.
103
+
104
+ ---
105
+
106
+ ## Full example
107
+
108
+ ```html
109
+ <div id="captureMe">
110
+ <h1 style="color: tomato;">Hello World!</h1>
111
+ <p>This content will be captured.</p>
112
+ </div>
113
+
114
+ <script type="module">
115
+ import { snapdom } from './snapdom.esm.js';
116
+
117
+ const button = document.createElement('button');
118
+ button.textContent = "Capture";
119
+ button.onclick = async () => {
120
+ const img = await snapdom.toPng(document.getElementById('captureMe'), 2);
121
+ document.body.appendChild(img);
122
+ };
123
+ document.body.appendChild(button);
124
+ </script>
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Limitations
130
+
131
+ - External images must be CORS-accessible.
132
+ - Fonts must be fully loaded before capturing (`document.fonts.ready` is automatically awaited).
133
+ - Iframes are not captured.
134
+
135
+ ---
136
+
137
+ ## Benchmark
138
+
139
+ `snapDOM` is not only highly accurate — it's also **extremely fast** at capturing large or complex DOM structures.
140
+
141
+ In benchmark tests against popular libraries:
142
+
143
+ | Element Size | Winner | Compared to `modern-screenshot` | Compared to `html2canvas` |
144
+ |:------------:|:------:|:-------------------------------:|:-------------------------:|
145
+ | 200×100 (Small) | `modern-screenshot` | 1.18× faster | 4.46× faster |
146
+ | 400×300 (Modal) | `snapDOM` | 1.04× faster | 4.07× faster |
147
+ | 1200×800 (Page view) | `snapDOM` | 2.43× faster | 5.74× faster |
148
+ | 2000×1500 (Large scroll area) | `snapDOM` | 5.02× faster | 9.35× faster |
149
+ | 4000×2000 (Very large) | `snapDOM` | 11.35× faster | 15.98× faster |
150
+
151
+ ✅ **Key insight**:
152
+ While `modern-screenshot` is yet slightly faster for very small elements, **snapDOM dramatically outperforms all others as the DOM size grows**.
153
+
154
+ ✅ **Perfect for:**
155
+ - Capturing full-page views
156
+ - Capturing modal windows
157
+ - Complex layouts with custom fonts, backgrounds, or shadow DOM
158
+
159
+ ---
160
+
161
+ ## License
162
+
163
+ MIT © Juan Martín Muda - Zumerlab
@@ -0,0 +1,524 @@
1
+
2
+ /*
3
+ * snapdom
4
+ * v.0.9.0
5
+ * Author Juan Martin Muda
6
+ * License MIT
7
+ **/
8
+ (() => {
9
+ // src/utils/cssTools.js
10
+ function getDefaultStyles() {
11
+ const defaultEl = document.createElement("div");
12
+ document.body.appendChild(defaultEl);
13
+ const styles = window.getComputedStyle(defaultEl);
14
+ const defaults = {};
15
+ for (let prop of styles) {
16
+ defaults[prop] = styles.getPropertyValue(prop);
17
+ }
18
+ document.body.removeChild(defaultEl);
19
+ return defaults;
20
+ }
21
+ function getStyleKey(style, defaults) {
22
+ const entries = [];
23
+ for (let prop of Array.from(style)) {
24
+ const value = style.getPropertyValue(prop);
25
+ if (value && value !== defaults[prop]) {
26
+ entries.push(`${prop}:${value}`);
27
+ }
28
+ }
29
+ return entries.sort().join(";");
30
+ }
31
+ function generateReusableCSSClasses(styleMap) {
32
+ const keySet = new Set(styleMap.values());
33
+ const classMap = /* @__PURE__ */ new Map();
34
+ let counter = 1;
35
+ for (const key of keySet) {
36
+ classMap.set(key, `c${counter++}`);
37
+ }
38
+ return classMap;
39
+ }
40
+
41
+ // src/modules/styles.js
42
+ function inlineAllStyles(source, clone, styleMap, defaults, cache) {
43
+ if (source.tagName === "STYLE") return;
44
+ if (!cache.has(source)) {
45
+ cache.set(source, window.getComputedStyle(source));
46
+ }
47
+ const style = cache.get(source);
48
+ const key = getStyleKey(style, defaults);
49
+ styleMap.set(clone, key);
50
+ }
51
+
52
+ // src/core/clone.js
53
+ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
54
+ if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
55
+ const spacer = document.createElement("div");
56
+ const rect = node.getBoundingClientRect();
57
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
58
+ return spacer;
59
+ }
60
+ if (node.tagName === "IFRAME") {
61
+ const fallback = document.createElement("div");
62
+ fallback.textContent = "";
63
+ fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`;
64
+ return fallback;
65
+ }
66
+ if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
67
+ const clone2 = node.cloneNode(false);
68
+ nodeMap.set(clone2, node);
69
+ inlineAllStyles(node, clone2, styleMap, defaults, styleCache);
70
+ const placeholder = document.createElement("div");
71
+ placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
72
+ placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
73
+ clone2.appendChild(placeholder);
74
+ return clone2;
75
+ }
76
+ if (node.tagName === "CANVAS") {
77
+ const dataURL = node.toDataURL();
78
+ const img = document.createElement("img");
79
+ img.src = dataURL;
80
+ img.width = node.width;
81
+ img.height = node.height;
82
+ return img;
83
+ }
84
+ if (node.nodeType === Node.TEXT_NODE) {
85
+ const trimmed = node.textContent.trim();
86
+ if (!trimmed) return null;
87
+ if (node.parentElement?.shadowRoot) {
88
+ const tag = node.parentElement.tagName.toLowerCase();
89
+ if (customElements.get(tag)) return null;
90
+ }
91
+ return node.cloneNode(true);
92
+ }
93
+ if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
94
+ const clone = node.cloneNode(false);
95
+ nodeMap.set(clone, node);
96
+ inlineAllStyles(node, clone, styleMap, defaults, styleCache);
97
+ const frag = document.createDocumentFragment();
98
+ node.childNodes.forEach((child) => {
99
+ const clonedChild = deepCloneWithShadow(child, styleMap, defaults, styleCache, nodeMap);
100
+ if (clonedChild) frag.appendChild(clonedChild);
101
+ });
102
+ clone.appendChild(frag);
103
+ if (node.shadowRoot) {
104
+ const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepCloneWithShadow(el, styleMap, defaults, styleCache, nodeMap)).filter(Boolean);
105
+ const shadowFrag = document.createDocumentFragment();
106
+ shadowContent.forEach((child) => shadowFrag.appendChild(child));
107
+ clone.appendChild(shadowFrag);
108
+ }
109
+ clone.querySelectorAll("style").forEach((tag) => tag.removeAttribute("style"));
110
+ if (clone.tagName === "STYLE") clone.removeAttribute("style");
111
+ return clone;
112
+ }
113
+
114
+ // src/utils/fetchImage.js
115
+ function fetchImageAsDataURL(src, timeout = 3e3) {
116
+ return new Promise((resolve, reject) => {
117
+ const timeoutId = setTimeout(() => {
118
+ reject(new Error("Image load timed out"));
119
+ }, timeout);
120
+ const image = new Image();
121
+ image.crossOrigin = "anonymous";
122
+ image.onload = async () => {
123
+ clearTimeout(timeoutId);
124
+ try {
125
+ await image.decode();
126
+ const canvas = document.createElement("canvas");
127
+ canvas.width = image.width;
128
+ canvas.height = image.height;
129
+ const ctx = canvas.getContext("2d");
130
+ ctx.drawImage(image, 0, 0);
131
+ try {
132
+ const dataURL = canvas.toDataURL("image/png");
133
+ resolve(dataURL);
134
+ } catch (e) {
135
+ reject(new Error("CORS restrictions prevented image capture"));
136
+ }
137
+ } catch (e) {
138
+ reject(e);
139
+ }
140
+ };
141
+ image.onerror = (e) => {
142
+ clearTimeout(timeoutId);
143
+ reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
144
+ };
145
+ image.src = src;
146
+ });
147
+ }
148
+
149
+ // src/utils/delay.js
150
+ function delay(ms = 0) {
151
+ return new Promise((resolve) => setTimeout(resolve, ms));
152
+ }
153
+
154
+ // src/modules/images.js
155
+ async function inlineImages(clone) {
156
+ const imgs = Array.from(clone.querySelectorAll("img"));
157
+ const processImg = async (img) => {
158
+ const src = img.src;
159
+ try {
160
+ const dataUrl = await fetchImageAsDataURL(src);
161
+ img.src = dataUrl;
162
+ if (!img.width) img.width = img.naturalWidth || 100;
163
+ if (!img.height) img.height = img.naturalHeight || 100;
164
+ } catch {
165
+ const fallback = document.createElement("div");
166
+ fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
167
+ fallback.innerText = "img";
168
+ img.replaceWith(fallback);
169
+ }
170
+ };
171
+ for (let i = 0; i < imgs.length; i += 4) {
172
+ const group = imgs.slice(i, i + 4).map(processImg);
173
+ await Promise.allSettled(group);
174
+ await delay(1);
175
+ }
176
+ }
177
+
178
+ // src/modules/background.js
179
+ async function inlineBackgroundImages(source, clone, styleCache) {
180
+ const queue = [[source, clone]];
181
+ while (queue.length) {
182
+ const [srcNode, cloneNode] = queue.shift();
183
+ const style = styleCache.get(srcNode) || window.getComputedStyle(srcNode);
184
+ if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
185
+ const bg = style.getPropertyValue("background-image");
186
+ if (bg && bg.includes("url(")) {
187
+ const match = bg.match(/url\(["']?([^"')]+)["']?\)/);
188
+ if (match?.[1]) {
189
+ try {
190
+ const dataUrl = await fetchImageAsDataURL(match[1]);
191
+ cloneNode.style.backgroundImage = `url(${dataUrl})`;
192
+ } catch {
193
+ cloneNode.style.backgroundImage = "none";
194
+ }
195
+ }
196
+ }
197
+ const sChildren = Array.from(srcNode.children);
198
+ const cChildren = Array.from(cloneNode.children);
199
+ for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
200
+ queue.push([sChildren[i], cChildren[i]]);
201
+ }
202
+ await delay(1);
203
+ }
204
+ }
205
+
206
+ // src/modules/pseudo.js
207
+ async function inlinePseudoElements(source, clone) {
208
+ const importantProps = [
209
+ "position",
210
+ "top",
211
+ "right",
212
+ "bottom",
213
+ "left",
214
+ "z-index",
215
+ "width",
216
+ "height",
217
+ "min-width",
218
+ "max-width",
219
+ "min-height",
220
+ "max-height",
221
+ "margin",
222
+ "padding",
223
+ "border",
224
+ "border-radius",
225
+ "background",
226
+ "background-color",
227
+ "background-image",
228
+ "color",
229
+ "font",
230
+ "font-family",
231
+ "font-size",
232
+ "font-weight",
233
+ "text-align",
234
+ "text-decoration",
235
+ "line-height",
236
+ "letter-spacing",
237
+ "display",
238
+ "visibility",
239
+ "opacity",
240
+ "overflow",
241
+ "box-shadow",
242
+ "transform",
243
+ "transition",
244
+ "flex",
245
+ "grid"
246
+ ];
247
+ for (const pseudo of ["::before", "::after"]) {
248
+ try {
249
+ const style = window.getComputedStyle(source, pseudo);
250
+ const content = style.getPropertyValue("content");
251
+ if (content && content !== "none" && content !== '""' && content !== "''") {
252
+ const pseudoEl = document.createElement("span");
253
+ pseudoEl.className = `snapdom-pseudo-${pseudo.replace("::", "")}`;
254
+ let cleanContent = content.replace(/^\"|\"$/g, "");
255
+ if (cleanContent.startsWith("url(")) {
256
+ const match = cleanContent.match(/url\(["']?([^"')]+)["']?\)/);
257
+ if (match?.[1]) {
258
+ try {
259
+ const imgEl = document.createElement("img");
260
+ const dataUrl = await fetchImageAsDataURL(match[1]);
261
+ imgEl.src = dataUrl;
262
+ imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
263
+ pseudoEl.appendChild(imgEl);
264
+ } catch (e) {
265
+ }
266
+ }
267
+ } else {
268
+ pseudoEl.textContent = cleanContent;
269
+ }
270
+ for (const prop of importantProps) {
271
+ const value = style.getPropertyValue(prop);
272
+ if (value) {
273
+ pseudoEl.style[prop] = value;
274
+ }
275
+ }
276
+ if (!pseudoEl.style.position) pseudoEl.style.position = "absolute";
277
+ if (pseudo === "::before") {
278
+ clone.insertBefore(pseudoEl, clone.firstChild);
279
+ } else {
280
+ clone.appendChild(pseudoEl);
281
+ }
282
+ }
283
+ } catch (e) {
284
+ }
285
+ }
286
+ const sChildren = Array.from(source.children);
287
+ const cChildren = Array.from(clone.children).filter(
288
+ (child) => !child.classList.contains("snapdom-pseudo-before") && !child.classList.contains("snapdom-pseudo-after")
289
+ );
290
+ for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
291
+ await inlinePseudoElements(sChildren[i], cChildren[i]);
292
+ if (i % 5 === 0) await delay(1);
293
+ }
294
+ }
295
+
296
+ // src/utils/normalize.js
297
+ function normalizeFontName(str) {
298
+ return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
299
+ }
300
+ function normalizeVariation(value) {
301
+ const mappings = {
302
+ "normal": "400",
303
+ "bold": "700",
304
+ "italic": "italic",
305
+ "oblique": "italic",
306
+ "ultra-condensed": "50",
307
+ "extra-condensed": "62",
308
+ "condensed": "75",
309
+ "semi-condensed": "87",
310
+ "expanded": "125",
311
+ "semi-expanded": "112",
312
+ "extra-expanded": "150",
313
+ "ultra-expanded": "200"
314
+ };
315
+ return mappings[value] || value.replace(/%/g, "");
316
+ }
317
+
318
+ // src/modules/fonts.js
319
+ async function inlineFonts(styleCache = /* @__PURE__ */ new WeakMap()) {
320
+ await document.fonts.ready;
321
+ const fontFaceRules = [];
322
+ Array.from(document.styleSheets).forEach((sheet) => {
323
+ try {
324
+ Array.from(sheet.cssRules).forEach((rule) => {
325
+ if (rule instanceof CSSFontFaceRule) {
326
+ fontFaceRules.push(rule.cssText);
327
+ }
328
+ });
329
+ } catch (e) {
330
+ console.warn("Could not access stylesheet rules:", e);
331
+ }
332
+ });
333
+ const fontData = /* @__PURE__ */ new Map();
334
+ const elements = document.querySelectorAll("*");
335
+ elements.forEach((el) => {
336
+ const style = styleCache.get(el) || window.getComputedStyle(el);
337
+ if (!styleCache.has(el)) styleCache.set(el, style);
338
+ const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
339
+ if (!family) return;
340
+ const weight = normalizeVariation(style.fontWeight || "400");
341
+ const styleAttr = normalizeVariation(style.fontStyle || "normal");
342
+ const stretch = normalizeVariation(style.fontStretch || "normal");
343
+ if (!fontData.has(family)) {
344
+ fontData.set(family, /* @__PURE__ */ new Set());
345
+ }
346
+ fontData.get(family).add(`${weight}_${styleAttr}_${stretch}`);
347
+ });
348
+ const cssClasses = [...fontFaceRules];
349
+ for (const [family, variations] of fontData) {
350
+ const safeFamily = normalizeFontName(family);
351
+ for (const variation of variations) {
352
+ const [weight, styleAttr, stretch] = variation.split("_");
353
+ const variationClass = `fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`;
354
+ const cssProps = [
355
+ `font-family:"${family}",sans-serif`,
356
+ `font-weight:${weight}`,
357
+ `font-style:${styleAttr}`,
358
+ `font-stretch:${stretch}%`
359
+ ].filter(Boolean).join(";");
360
+ cssClasses.push(`.${variationClass}{${cssProps}}`);
361
+ }
362
+ }
363
+ cssClasses.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}");
364
+ return cssClasses.join("");
365
+ }
366
+ function applyFontClasses(clone, original, styleCache) {
367
+ const style = styleCache.get(original) || window.getComputedStyle(original);
368
+ const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
369
+ if (!family) return;
370
+ const weight = normalizeVariation(style.fontWeight || "400");
371
+ const styleAttr = normalizeVariation(style.fontStyle || "normal");
372
+ const stretch = normalizeVariation(style.fontStretch || "normal");
373
+ const safeFamily = normalizeFontName(family);
374
+ clone.classList.add(`fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`);
375
+ }
376
+
377
+ // src/core/prepare.js
378
+ async function prepareClone(element) {
379
+ const styleMap = /* @__PURE__ */ new Map();
380
+ const styleCache = /* @__PURE__ */ new WeakMap();
381
+ const nodeMap = /* @__PURE__ */ new WeakMap();
382
+ let defaults;
383
+ try {
384
+ defaults = getDefaultStyles();
385
+ } catch (e) {
386
+ console.warn("getDefaultStyles failed:", e);
387
+ defaults = {};
388
+ }
389
+ let clone;
390
+ try {
391
+ clone = deepCloneWithShadow(element, styleMap, defaults, styleCache, nodeMap);
392
+ } catch (e) {
393
+ console.warn("deepCloneWithShadow failed:", e);
394
+ throw e;
395
+ }
396
+ try {
397
+ await inlineImages(clone);
398
+ } catch (e) {
399
+ console.warn("inlineImages failed:", e);
400
+ }
401
+ try {
402
+ await inlineBackgroundImages(element, clone, styleCache);
403
+ } catch (e) {
404
+ console.warn("inlineBackgroundImages failed:", e);
405
+ }
406
+ try {
407
+ await inlinePseudoElements(element, clone);
408
+ } catch (e) {
409
+ console.warn("inlinePseudoElements failed:", e);
410
+ }
411
+ const keyToClass = generateReusableCSSClasses(styleMap);
412
+ const classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
413
+ for (const [node, key] of styleMap.entries()) {
414
+ if (node.tagName === "STYLE") continue;
415
+ const className = keyToClass.get(key);
416
+ if (className) {
417
+ node.classList.add(className);
418
+ try {
419
+ applyFontClasses(node, nodeMap.get(node), styleCache);
420
+ } catch (e) {
421
+ console.warn("applyFontClasses failed for node:", node, e);
422
+ }
423
+ }
424
+ const bgImage = node.style?.backgroundImage;
425
+ node.removeAttribute("style");
426
+ if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
427
+ }
428
+ return { clone, classCSS, styleCache };
429
+ }
430
+
431
+ // src/utils/minifySVG.js
432
+ function minifySVG(svgString) {
433
+ return svgString.replace(/>\s+</g, "><").replace(/\s{2,}/g, " ").replace(/\n/g, "").trim();
434
+ }
435
+
436
+ // src/core/capture.js
437
+ async function capture(element, scale = 1) {
438
+ let clone, classCSS, styleCache;
439
+ try {
440
+ ({ clone, classCSS, styleCache } = await prepareClone(element));
441
+ } catch (e) {
442
+ console.error("prepareClone failed:", e);
443
+ throw e;
444
+ }
445
+ const w = element.offsetWidth * scale;
446
+ const h = element.offsetHeight * scale;
447
+ let fonts = "";
448
+ try {
449
+ fonts = await inlineFonts(styleCache);
450
+ } catch (e) {
451
+ console.warn("inlineFonts failed:", e);
452
+ }
453
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
454
+ svg.setAttribute("width", w);
455
+ svg.setAttribute("height", h);
456
+ svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
457
+ const fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
458
+ fo.setAttribute("width", "100%");
459
+ fo.setAttribute("height", "100%");
460
+ const div = document.createElement("div");
461
+ div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
462
+ div.setAttribute("style", `width:${w}px;height:${h}px;`);
463
+ const styleTag = document.createElement("style");
464
+ styleTag.textContent = fonts + "svg{overflow:visible}" + classCSS;
465
+ div.appendChild(styleTag);
466
+ div.appendChild(clone);
467
+ fo.appendChild(div);
468
+ svg.appendChild(fo);
469
+ try {
470
+ const svgStr = new XMLSerializer().serializeToString(svg);
471
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(minifySVG(svgStr))}`;
472
+ } catch (e) {
473
+ console.error("SVG serialization failed:", e);
474
+ throw e;
475
+ }
476
+ }
477
+
478
+ // src/api/snapdom.js
479
+ async function snapdom(el, scale = 1) {
480
+ return await capture(el, scale);
481
+ }
482
+ snapdom.toImg = async function(el, scale = 1) {
483
+ const dataURL = await snapdom(el, scale);
484
+ const img = new Image();
485
+ img.src = dataURL;
486
+ await img.decode();
487
+ return img;
488
+ };
489
+ snapdom.toCanvas = async function(el, scale = 1) {
490
+ const img = await snapdom.toImg(el, scale);
491
+ const canvas = document.createElement("canvas");
492
+ canvas.width = img.width;
493
+ canvas.height = img.height;
494
+ const ctx = canvas.getContext("2d");
495
+ ctx.drawImage(img, 0, 0);
496
+ return canvas;
497
+ };
498
+ snapdom.toPng = async function(el, scale = 1) {
499
+ const canvas = await snapdom.toCanvas(el, scale);
500
+ const img = new Image();
501
+ img.src = canvas.toDataURL("image/png");
502
+ await img.decode();
503
+ return img;
504
+ };
505
+ snapdom.toJpg = async function(el, scale = 1, quality = 0.92) {
506
+ const canvas = await snapdom.toCanvas(el, scale);
507
+ const img = new Image();
508
+ img.src = canvas.toDataURL("image/jpeg", quality);
509
+ await img.decode();
510
+ return img;
511
+ };
512
+ snapdom.toWebp = async function(el, scale = 1, quality = 0.92) {
513
+ const canvas = await snapdom.toCanvas(el, scale);
514
+ const img = new Image();
515
+ img.src = canvas.toDataURL("image/webp", quality);
516
+ await img.decode();
517
+ return img;
518
+ };
519
+ snapdom.toBlob = async function(el, scale = 1) {
520
+ const dataURL = await snapdom(el, scale);
521
+ const svgXml = decodeURIComponent(dataURL.split(",")[1]);
522
+ return new Blob([svgXml], { type: "image/svg+xml" });
523
+ };
524
+ })();
@@ -0,0 +1 @@
1
+ (()=>{function b(){let t=document.createElement("div");document.body.appendChild(t);let a=window.getComputedStyle(t),r={};for(let e of a)r[e]=a.getPropertyValue(e);return document.body.removeChild(t),r}function S(t,a){let r=[];for(let e of Array.from(t)){let i=t.getPropertyValue(e);i&&i!==a[e]&&r.push(`${e}:${i}`)}return r.sort().join(";")}function E(t){let a=new Set(t.values()),r=new Map,e=1;for(let i of a)r.set(i,`c${e++}`);return r}function w(t,a,r,e,i){if(t.tagName==="STYLE")return;i.has(t)||i.set(t,window.getComputedStyle(t));let o=i.get(t),c=S(o,e);r.set(a,c)}function y(t,a,r,e,i){if(t.nodeType===Node.ELEMENT_NODE&&t.getAttribute("data-capture")==="exclude"){let s=document.createElement("div"),n=t.getBoundingClientRect();return s.style.cssText=`display: inline-block; width: ${n.width}px; height: ${n.height}px; visibility: hidden;`,s}if(t.tagName==="IFRAME"){let s=document.createElement("div");return s.textContent="",s.style.cssText=`width: ${t.offsetWidth}px; height: ${t.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`,s}if(t.nodeType===Node.ELEMENT_NODE&&t.getAttribute("data-capture")==="placeholder"){let s=t.cloneNode(!1);i.set(s,t),w(t,s,a,r,e);let n=document.createElement("div");return n.textContent=t.getAttribute("data-placeholder-text")||"",n.style.cssText="color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;",s.appendChild(n),s}if(t.tagName==="CANVAS"){let s=t.toDataURL(),n=document.createElement("img");return n.src=s,n.width=t.width,n.height=t.height,n}if(t.nodeType===Node.TEXT_NODE){if(!t.textContent.trim())return null;if(t.parentElement?.shadowRoot){let n=t.parentElement.tagName.toLowerCase();if(customElements.get(n))return null}return t.cloneNode(!0)}if(t.nodeType!==Node.ELEMENT_NODE)return t.cloneNode(!0);let o=t.cloneNode(!1);i.set(o,t),w(t,o,a,r,e);let c=document.createDocumentFragment();if(t.childNodes.forEach(s=>{let n=y(s,a,r,e,i);n&&c.appendChild(n)}),o.appendChild(c),t.shadowRoot){let s=Array.from(t.shadowRoot.children).filter(d=>d.tagName!=="STYLE").map(d=>y(d,a,r,e,i)).filter(Boolean),n=document.createDocumentFragment();s.forEach(d=>n.appendChild(d)),o.appendChild(n)}return o.querySelectorAll("style").forEach(s=>s.removeAttribute("style")),o.tagName==="STYLE"&&o.removeAttribute("style"),o}function h(t,a=3e3){return new Promise((r,e)=>{let i=setTimeout(()=>{e(new Error("Image load timed out"))},a),o=new Image;o.crossOrigin="anonymous",o.onload=async()=>{clearTimeout(i);try{await o.decode();let c=document.createElement("canvas");c.width=o.width,c.height=o.height,c.getContext("2d").drawImage(o,0,0);try{let n=c.toDataURL("image/png");r(n)}catch{e(new Error("CORS restrictions prevented image capture"))}}catch(c){e(c)}},o.onerror=c=>{clearTimeout(i),e(new Error("Failed to load image: "+(c.message||"Unknown error")))},o.src=t})}function u(t=0){return new Promise(a=>setTimeout(a,t))}async function v(t){let a=Array.from(t.querySelectorAll("img")),r=async e=>{let i=e.src;try{let o=await h(i);e.src=o,e.width||(e.width=e.naturalWidth||100),e.height||(e.height=e.naturalHeight||100)}catch{let o=document.createElement("div");o.style=`width: ${e.width||100}px; height: ${e.height||100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${e.height||100}px; color: #666; font-size: 12px;`,o.innerText="img",e.replaceWith(o)}};for(let e=0;e<a.length;e+=4){let i=a.slice(e,e+4).map(r);await Promise.allSettled(i),await u(1)}}async function A(t,a,r){let e=[[t,a]];for(;e.length;){let[i,o]=e.shift(),c=r.get(i)||window.getComputedStyle(i);r.has(i)||r.set(i,c);let s=c.getPropertyValue("background-image");if(s&&s.includes("url(")){let l=s.match(/url\(["']?([^"')]+)["']?\)/);if(l?.[1])try{let m=await h(l[1]);o.style.backgroundImage=`url(${m})`}catch{o.style.backgroundImage="none"}}let n=Array.from(i.children),d=Array.from(o.children);for(let l=0;l<Math.min(n.length,d.length);l++)e.push([n[l],d[l]]);await u(1)}}async function x(t,a){let r=["position","top","right","bottom","left","z-index","width","height","min-width","max-width","min-height","max-height","margin","padding","border","border-radius","background","background-color","background-image","color","font","font-family","font-size","font-weight","text-align","text-decoration","line-height","letter-spacing","display","visibility","opacity","overflow","box-shadow","transform","transition","flex","grid"];for(let o of["::before","::after"])try{let c=window.getComputedStyle(t,o),s=c.getPropertyValue("content");if(s&&s!=="none"&&s!=='""'&&s!=="''"){let n=document.createElement("span");n.className=`snapdom-pseudo-${o.replace("::","")}`;let d=s.replace(/^\"|\"$/g,"");if(d.startsWith("url(")){let l=d.match(/url\(["']?([^"')]+)["']?\)/);if(l?.[1])try{let m=document.createElement("img"),f=await h(l[1]);m.src=f,m.style="display:block;width:100%;height:100%;object-fit:contain;",n.appendChild(m)}catch{}}else n.textContent=d;for(let l of r){let m=c.getPropertyValue(l);m&&(n.style[l]=m)}n.style.position||(n.style.position="absolute"),o==="::before"?a.insertBefore(n,a.firstChild):a.appendChild(n)}}catch{}let e=Array.from(t.children),i=Array.from(a.children).filter(o=>!o.classList.contains("snapdom-pseudo-before")&&!o.classList.contains("snapdom-pseudo-after"));for(let o=0;o<Math.min(e.length,i.length);o++)await x(e[o],i[o]),o%5===0&&await u(1)}function C(t){return t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function g(t){return{normal:"400",bold:"700",italic:"italic",oblique:"italic","ultra-condensed":"50","extra-condensed":"62",condensed:"75","semi-condensed":"87",expanded:"125","semi-expanded":"112","extra-expanded":"150","ultra-expanded":"200"}[t]||t.replace(/%/g,"")}async function $(t=new WeakMap){await document.fonts.ready;let a=[];Array.from(document.styleSheets).forEach(o=>{try{Array.from(o.cssRules).forEach(c=>{c instanceof CSSFontFaceRule&&a.push(c.cssText)})}catch(c){console.warn("Could not access stylesheet rules:",c)}});let r=new Map;document.querySelectorAll("*").forEach(o=>{let c=t.get(o)||window.getComputedStyle(o);t.has(o)||t.set(o,c);let s=c.fontFamily?.split(",")[0]?.trim().replace(/["']/g,"");if(!s)return;let n=g(c.fontWeight||"400"),d=g(c.fontStyle||"normal"),l=g(c.fontStretch||"normal");r.has(s)||r.set(s,new Set),r.get(s).add(`${n}_${d}_${l}`)});let i=[...a];for(let[o,c]of r){let s=C(o);for(let n of c){let[d,l,m]=n.split("_"),f=`fg-f-${s}-w${d}-${l}-s${m}`,R=[`font-family:"${o}",sans-serif`,`font-weight:${d}`,`font-style:${l}`,`font-stretch:${m}%`].filter(Boolean).join(";");i.push(`.${f}{${R}}`)}}return i.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}"),i.join("")}function N(t,a,r){let e=r.get(a)||window.getComputedStyle(a),i=e.fontFamily?.split(",")[0]?.trim().replace(/["']/g,"");if(!i)return;let o=g(e.fontWeight||"400"),c=g(e.fontStyle||"normal"),s=g(e.fontStretch||"normal"),n=C(i);t.classList.add(`fg-f-${n}-w${o}-${c}-s${s}`)}async function I(t){let a=new Map,r=new WeakMap,e=new WeakMap,i;try{i=b()}catch(n){console.warn("getDefaultStyles failed:",n),i={}}let o;try{o=y(t,a,i,r,e)}catch(n){throw console.warn("deepCloneWithShadow failed:",n),n}try{await v(o)}catch(n){console.warn("inlineImages failed:",n)}try{await A(t,o,r)}catch(n){console.warn("inlineBackgroundImages failed:",n)}try{await x(t,o)}catch(n){console.warn("inlinePseudoElements failed:",n)}let c=E(a),s=Array.from(c.entries()).map(([n,d])=>`.${d}{${n}}`).join("");for(let[n,d]of a.entries()){if(n.tagName==="STYLE")continue;let l=c.get(d);if(l){n.classList.add(l);try{N(n,e.get(n),r)}catch(f){console.warn("applyFontClasses failed for node:",n,f)}}let m=n.style?.backgroundImage;n.removeAttribute("style"),m&&m!=="none"&&(n.style.backgroundImage=m)}return{clone:o,classCSS:s,styleCache:r}}function k(t){return t.replace(/>\s+</g,"><").replace(/\s{2,}/g," ").replace(/\n/g,"").trim()}async function L(t,a=1){let r,e,i;try{({clone:r,classCSS:e,styleCache:i}=await I(t))}catch(f){throw console.error("prepareClone failed:",f),f}let o=t.offsetWidth*a,c=t.offsetHeight*a,s="";try{s=await $(i)}catch(f){console.warn("inlineFonts failed:",f)}let n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width",o),n.setAttribute("height",c),n.setAttribute("viewBox",`0 0 ${o} ${c}`);let d=document.createElementNS("http://www.w3.org/2000/svg","foreignObject");d.setAttribute("width","100%"),d.setAttribute("height","100%");let l=document.createElement("div");l.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),l.setAttribute("style",`width:${o}px;height:${c}px;`);let m=document.createElement("style");m.textContent=s+"svg{overflow:visible}"+e,l.appendChild(m),l.appendChild(r),d.appendChild(l),n.appendChild(d);try{let f=new XMLSerializer().serializeToString(n);return`data:image/svg+xml;charset=utf-8,${encodeURIComponent(k(f))}`}catch(f){throw console.error("SVG serialization failed:",f),f}}async function p(t,a=1){return await L(t,a)}p.toImg=async function(t,a=1){let r=await p(t,a),e=new Image;return e.src=r,await e.decode(),e};p.toCanvas=async function(t,a=1){let r=await p.toImg(t,a),e=document.createElement("canvas");return e.width=r.width,e.height=r.height,e.getContext("2d").drawImage(r,0,0),e};p.toPng=async function(t,a=1){let r=await p.toCanvas(t,a),e=new Image;return e.src=r.toDataURL("image/png"),await e.decode(),e};p.toJpg=async function(t,a=1,r=.92){let e=await p.toCanvas(t,a),i=new Image;return i.src=e.toDataURL("image/jpeg",r),await i.decode(),i};p.toWebp=async function(t,a=1,r=.92){let e=await p.toCanvas(t,a),i=new Image;return i.src=e.toDataURL("image/webp",r),await i.decode(),i};p.toBlob=async function(t,a=1){let r=await p(t,a),e=decodeURIComponent(r.split(",")[1]);return new Blob([e],{type:"image/svg+xml"})};})();
@@ -0,0 +1 @@
1
+ function b(){let t=document.createElement("div");document.body.appendChild(t);let a=window.getComputedStyle(t),r={};for(let e of a)r[e]=a.getPropertyValue(e);return document.body.removeChild(t),r}function S(t,a){let r=[];for(let e of Array.from(t)){let i=t.getPropertyValue(e);i&&i!==a[e]&&r.push(`${e}:${i}`)}return r.sort().join(";")}function E(t){let a=new Set(t.values()),r=new Map,e=1;for(let i of a)r.set(i,`c${e++}`);return r}function w(t,a,r,e,i){if(t.tagName==="STYLE")return;i.has(t)||i.set(t,window.getComputedStyle(t));let o=i.get(t),c=S(o,e);r.set(a,c)}function y(t,a,r,e,i){if(t.nodeType===Node.ELEMENT_NODE&&t.getAttribute("data-capture")==="exclude"){let s=document.createElement("div"),n=t.getBoundingClientRect();return s.style.cssText=`display: inline-block; width: ${n.width}px; height: ${n.height}px; visibility: hidden;`,s}if(t.tagName==="IFRAME"){let s=document.createElement("div");return s.textContent="",s.style.cssText=`width: ${t.offsetWidth}px; height: ${t.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`,s}if(t.nodeType===Node.ELEMENT_NODE&&t.getAttribute("data-capture")==="placeholder"){let s=t.cloneNode(!1);i.set(s,t),w(t,s,a,r,e);let n=document.createElement("div");return n.textContent=t.getAttribute("data-placeholder-text")||"",n.style.cssText="color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;",s.appendChild(n),s}if(t.tagName==="CANVAS"){let s=t.toDataURL(),n=document.createElement("img");return n.src=s,n.width=t.width,n.height=t.height,n}if(t.nodeType===Node.TEXT_NODE){if(!t.textContent.trim())return null;if(t.parentElement?.shadowRoot){let n=t.parentElement.tagName.toLowerCase();if(customElements.get(n))return null}return t.cloneNode(!0)}if(t.nodeType!==Node.ELEMENT_NODE)return t.cloneNode(!0);let o=t.cloneNode(!1);i.set(o,t),w(t,o,a,r,e);let c=document.createDocumentFragment();if(t.childNodes.forEach(s=>{let n=y(s,a,r,e,i);n&&c.appendChild(n)}),o.appendChild(c),t.shadowRoot){let s=Array.from(t.shadowRoot.children).filter(d=>d.tagName!=="STYLE").map(d=>y(d,a,r,e,i)).filter(Boolean),n=document.createDocumentFragment();s.forEach(d=>n.appendChild(d)),o.appendChild(n)}return o.querySelectorAll("style").forEach(s=>s.removeAttribute("style")),o.tagName==="STYLE"&&o.removeAttribute("style"),o}function h(t,a=3e3){return new Promise((r,e)=>{let i=setTimeout(()=>{e(new Error("Image load timed out"))},a),o=new Image;o.crossOrigin="anonymous",o.onload=async()=>{clearTimeout(i);try{await o.decode();let c=document.createElement("canvas");c.width=o.width,c.height=o.height,c.getContext("2d").drawImage(o,0,0);try{let n=c.toDataURL("image/png");r(n)}catch{e(new Error("CORS restrictions prevented image capture"))}}catch(c){e(c)}},o.onerror=c=>{clearTimeout(i),e(new Error("Failed to load image: "+(c.message||"Unknown error")))},o.src=t})}function u(t=0){return new Promise(a=>setTimeout(a,t))}async function v(t){let a=Array.from(t.querySelectorAll("img")),r=async e=>{let i=e.src;try{let o=await h(i);e.src=o,e.width||(e.width=e.naturalWidth||100),e.height||(e.height=e.naturalHeight||100)}catch{let o=document.createElement("div");o.style=`width: ${e.width||100}px; height: ${e.height||100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${e.height||100}px; color: #666; font-size: 12px;`,o.innerText="img",e.replaceWith(o)}};for(let e=0;e<a.length;e+=4){let i=a.slice(e,e+4).map(r);await Promise.allSettled(i),await u(1)}}async function A(t,a,r){let e=[[t,a]];for(;e.length;){let[i,o]=e.shift(),c=r.get(i)||window.getComputedStyle(i);r.has(i)||r.set(i,c);let s=c.getPropertyValue("background-image");if(s&&s.includes("url(")){let l=s.match(/url\(["']?([^"')]+)["']?\)/);if(l?.[1])try{let m=await h(l[1]);o.style.backgroundImage=`url(${m})`}catch{o.style.backgroundImage="none"}}let n=Array.from(i.children),d=Array.from(o.children);for(let l=0;l<Math.min(n.length,d.length);l++)e.push([n[l],d[l]]);await u(1)}}async function x(t,a){let r=["position","top","right","bottom","left","z-index","width","height","min-width","max-width","min-height","max-height","margin","padding","border","border-radius","background","background-color","background-image","color","font","font-family","font-size","font-weight","text-align","text-decoration","line-height","letter-spacing","display","visibility","opacity","overflow","box-shadow","transform","transition","flex","grid"];for(let o of["::before","::after"])try{let c=window.getComputedStyle(t,o),s=c.getPropertyValue("content");if(s&&s!=="none"&&s!=='""'&&s!=="''"){let n=document.createElement("span");n.className=`snapdom-pseudo-${o.replace("::","")}`;let d=s.replace(/^\"|\"$/g,"");if(d.startsWith("url(")){let l=d.match(/url\(["']?([^"')]+)["']?\)/);if(l?.[1])try{let m=document.createElement("img"),f=await h(l[1]);m.src=f,m.style="display:block;width:100%;height:100%;object-fit:contain;",n.appendChild(m)}catch{}}else n.textContent=d;for(let l of r){let m=c.getPropertyValue(l);m&&(n.style[l]=m)}n.style.position||(n.style.position="absolute"),o==="::before"?a.insertBefore(n,a.firstChild):a.appendChild(n)}}catch{}let e=Array.from(t.children),i=Array.from(a.children).filter(o=>!o.classList.contains("snapdom-pseudo-before")&&!o.classList.contains("snapdom-pseudo-after"));for(let o=0;o<Math.min(e.length,i.length);o++)await x(e[o],i[o]),o%5===0&&await u(1)}function C(t){return t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function g(t){return{normal:"400",bold:"700",italic:"italic",oblique:"italic","ultra-condensed":"50","extra-condensed":"62",condensed:"75","semi-condensed":"87",expanded:"125","semi-expanded":"112","extra-expanded":"150","ultra-expanded":"200"}[t]||t.replace(/%/g,"")}async function $(t=new WeakMap){await document.fonts.ready;let a=[];Array.from(document.styleSheets).forEach(o=>{try{Array.from(o.cssRules).forEach(c=>{c instanceof CSSFontFaceRule&&a.push(c.cssText)})}catch(c){console.warn("Could not access stylesheet rules:",c)}});let r=new Map;document.querySelectorAll("*").forEach(o=>{let c=t.get(o)||window.getComputedStyle(o);t.has(o)||t.set(o,c);let s=c.fontFamily?.split(",")[0]?.trim().replace(/["']/g,"");if(!s)return;let n=g(c.fontWeight||"400"),d=g(c.fontStyle||"normal"),l=g(c.fontStretch||"normal");r.has(s)||r.set(s,new Set),r.get(s).add(`${n}_${d}_${l}`)});let i=[...a];for(let[o,c]of r){let s=C(o);for(let n of c){let[d,l,m]=n.split("_"),f=`fg-f-${s}-w${d}-${l}-s${m}`,R=[`font-family:"${o}",sans-serif`,`font-weight:${d}`,`font-style:${l}`,`font-stretch:${m}%`].filter(Boolean).join(";");i.push(`.${f}{${R}}`)}}return i.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}"),i.join("")}function N(t,a,r){let e=r.get(a)||window.getComputedStyle(a),i=e.fontFamily?.split(",")[0]?.trim().replace(/["']/g,"");if(!i)return;let o=g(e.fontWeight||"400"),c=g(e.fontStyle||"normal"),s=g(e.fontStretch||"normal"),n=C(i);t.classList.add(`fg-f-${n}-w${o}-${c}-s${s}`)}async function I(t){let a=new Map,r=new WeakMap,e=new WeakMap,i;try{i=b()}catch(n){console.warn("getDefaultStyles failed:",n),i={}}let o;try{o=y(t,a,i,r,e)}catch(n){throw console.warn("deepCloneWithShadow failed:",n),n}try{await v(o)}catch(n){console.warn("inlineImages failed:",n)}try{await A(t,o,r)}catch(n){console.warn("inlineBackgroundImages failed:",n)}try{await x(t,o)}catch(n){console.warn("inlinePseudoElements failed:",n)}let c=E(a),s=Array.from(c.entries()).map(([n,d])=>`.${d}{${n}}`).join("");for(let[n,d]of a.entries()){if(n.tagName==="STYLE")continue;let l=c.get(d);if(l){n.classList.add(l);try{N(n,e.get(n),r)}catch(f){console.warn("applyFontClasses failed for node:",n,f)}}let m=n.style?.backgroundImage;n.removeAttribute("style"),m&&m!=="none"&&(n.style.backgroundImage=m)}return{clone:o,classCSS:s,styleCache:r}}function k(t){return t.replace(/>\s+</g,"><").replace(/\s{2,}/g," ").replace(/\n/g,"").trim()}async function L(t,a=1){let r,e,i;try{({clone:r,classCSS:e,styleCache:i}=await I(t))}catch(f){throw console.error("prepareClone failed:",f),f}let o=t.offsetWidth*a,c=t.offsetHeight*a,s="";try{s=await $(i)}catch(f){console.warn("inlineFonts failed:",f)}let n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width",o),n.setAttribute("height",c),n.setAttribute("viewBox",`0 0 ${o} ${c}`);let d=document.createElementNS("http://www.w3.org/2000/svg","foreignObject");d.setAttribute("width","100%"),d.setAttribute("height","100%");let l=document.createElement("div");l.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),l.setAttribute("style",`width:${o}px;height:${c}px;`);let m=document.createElement("style");m.textContent=s+"svg{overflow:visible}"+e,l.appendChild(m),l.appendChild(r),d.appendChild(l),n.appendChild(d);try{let f=new XMLSerializer().serializeToString(n);return`data:image/svg+xml;charset=utf-8,${encodeURIComponent(k(f))}`}catch(f){throw console.error("SVG serialization failed:",f),f}}async function p(t,a=1){return await L(t,a)}p.toImg=async function(t,a=1){let r=await p(t,a),e=new Image;return e.src=r,await e.decode(),e};p.toCanvas=async function(t,a=1){let r=await p.toImg(t,a),e=document.createElement("canvas");return e.width=r.width,e.height=r.height,e.getContext("2d").drawImage(r,0,0),e};p.toPng=async function(t,a=1){let r=await p.toCanvas(t,a),e=new Image;return e.src=r.toDataURL("image/png"),await e.decode(),e};p.toJpg=async function(t,a=1,r=.92){let e=await p.toCanvas(t,a),i=new Image;return i.src=e.toDataURL("image/jpeg",r),await i.decode(),i};p.toWebp=async function(t,a=1,r=.92){let e=await p.toCanvas(t,a),i=new Image;return i.src=e.toDataURL("image/webp",r),await i.decode(),i};p.toBlob=async function(t,a=1){let r=await p(t,a),e=decodeURIComponent(r.split(",")[1]);return new Blob([e],{type:"image/svg+xml"})};export{p as snapdom};
@@ -0,0 +1,526 @@
1
+
2
+ /*
3
+ * snapdom
4
+ * v.0.9.0
5
+ * Author Juan Martin Muda
6
+ * License MIT
7
+ **/
8
+
9
+ // src/utils/cssTools.js
10
+ function getDefaultStyles() {
11
+ const defaultEl = document.createElement("div");
12
+ document.body.appendChild(defaultEl);
13
+ const styles = window.getComputedStyle(defaultEl);
14
+ const defaults = {};
15
+ for (let prop of styles) {
16
+ defaults[prop] = styles.getPropertyValue(prop);
17
+ }
18
+ document.body.removeChild(defaultEl);
19
+ return defaults;
20
+ }
21
+ function getStyleKey(style, defaults) {
22
+ const entries = [];
23
+ for (let prop of Array.from(style)) {
24
+ const value = style.getPropertyValue(prop);
25
+ if (value && value !== defaults[prop]) {
26
+ entries.push(`${prop}:${value}`);
27
+ }
28
+ }
29
+ return entries.sort().join(";");
30
+ }
31
+ function generateReusableCSSClasses(styleMap) {
32
+ const keySet = new Set(styleMap.values());
33
+ const classMap = /* @__PURE__ */ new Map();
34
+ let counter = 1;
35
+ for (const key of keySet) {
36
+ classMap.set(key, `c${counter++}`);
37
+ }
38
+ return classMap;
39
+ }
40
+
41
+ // src/modules/styles.js
42
+ function inlineAllStyles(source, clone, styleMap, defaults, cache) {
43
+ if (source.tagName === "STYLE") return;
44
+ if (!cache.has(source)) {
45
+ cache.set(source, window.getComputedStyle(source));
46
+ }
47
+ const style = cache.get(source);
48
+ const key = getStyleKey(style, defaults);
49
+ styleMap.set(clone, key);
50
+ }
51
+
52
+ // src/core/clone.js
53
+ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
54
+ if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
55
+ const spacer = document.createElement("div");
56
+ const rect = node.getBoundingClientRect();
57
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
58
+ return spacer;
59
+ }
60
+ if (node.tagName === "IFRAME") {
61
+ const fallback = document.createElement("div");
62
+ fallback.textContent = "";
63
+ fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`;
64
+ return fallback;
65
+ }
66
+ if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
67
+ const clone2 = node.cloneNode(false);
68
+ nodeMap.set(clone2, node);
69
+ inlineAllStyles(node, clone2, styleMap, defaults, styleCache);
70
+ const placeholder = document.createElement("div");
71
+ placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
72
+ placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
73
+ clone2.appendChild(placeholder);
74
+ return clone2;
75
+ }
76
+ if (node.tagName === "CANVAS") {
77
+ const dataURL = node.toDataURL();
78
+ const img = document.createElement("img");
79
+ img.src = dataURL;
80
+ img.width = node.width;
81
+ img.height = node.height;
82
+ return img;
83
+ }
84
+ if (node.nodeType === Node.TEXT_NODE) {
85
+ const trimmed = node.textContent.trim();
86
+ if (!trimmed) return null;
87
+ if (node.parentElement?.shadowRoot) {
88
+ const tag = node.parentElement.tagName.toLowerCase();
89
+ if (customElements.get(tag)) return null;
90
+ }
91
+ return node.cloneNode(true);
92
+ }
93
+ if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
94
+ const clone = node.cloneNode(false);
95
+ nodeMap.set(clone, node);
96
+ inlineAllStyles(node, clone, styleMap, defaults, styleCache);
97
+ const frag = document.createDocumentFragment();
98
+ node.childNodes.forEach((child) => {
99
+ const clonedChild = deepCloneWithShadow(child, styleMap, defaults, styleCache, nodeMap);
100
+ if (clonedChild) frag.appendChild(clonedChild);
101
+ });
102
+ clone.appendChild(frag);
103
+ if (node.shadowRoot) {
104
+ const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepCloneWithShadow(el, styleMap, defaults, styleCache, nodeMap)).filter(Boolean);
105
+ const shadowFrag = document.createDocumentFragment();
106
+ shadowContent.forEach((child) => shadowFrag.appendChild(child));
107
+ clone.appendChild(shadowFrag);
108
+ }
109
+ clone.querySelectorAll("style").forEach((tag) => tag.removeAttribute("style"));
110
+ if (clone.tagName === "STYLE") clone.removeAttribute("style");
111
+ return clone;
112
+ }
113
+
114
+ // src/utils/fetchImage.js
115
+ function fetchImageAsDataURL(src, timeout = 3e3) {
116
+ return new Promise((resolve, reject) => {
117
+ const timeoutId = setTimeout(() => {
118
+ reject(new Error("Image load timed out"));
119
+ }, timeout);
120
+ const image = new Image();
121
+ image.crossOrigin = "anonymous";
122
+ image.onload = async () => {
123
+ clearTimeout(timeoutId);
124
+ try {
125
+ await image.decode();
126
+ const canvas = document.createElement("canvas");
127
+ canvas.width = image.width;
128
+ canvas.height = image.height;
129
+ const ctx = canvas.getContext("2d");
130
+ ctx.drawImage(image, 0, 0);
131
+ try {
132
+ const dataURL = canvas.toDataURL("image/png");
133
+ resolve(dataURL);
134
+ } catch (e) {
135
+ reject(new Error("CORS restrictions prevented image capture"));
136
+ }
137
+ } catch (e) {
138
+ reject(e);
139
+ }
140
+ };
141
+ image.onerror = (e) => {
142
+ clearTimeout(timeoutId);
143
+ reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
144
+ };
145
+ image.src = src;
146
+ });
147
+ }
148
+
149
+ // src/utils/delay.js
150
+ function delay(ms = 0) {
151
+ return new Promise((resolve) => setTimeout(resolve, ms));
152
+ }
153
+
154
+ // src/modules/images.js
155
+ async function inlineImages(clone) {
156
+ const imgs = Array.from(clone.querySelectorAll("img"));
157
+ const processImg = async (img) => {
158
+ const src = img.src;
159
+ try {
160
+ const dataUrl = await fetchImageAsDataURL(src);
161
+ img.src = dataUrl;
162
+ if (!img.width) img.width = img.naturalWidth || 100;
163
+ if (!img.height) img.height = img.naturalHeight || 100;
164
+ } catch {
165
+ const fallback = document.createElement("div");
166
+ fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
167
+ fallback.innerText = "img";
168
+ img.replaceWith(fallback);
169
+ }
170
+ };
171
+ for (let i = 0; i < imgs.length; i += 4) {
172
+ const group = imgs.slice(i, i + 4).map(processImg);
173
+ await Promise.allSettled(group);
174
+ await delay(1);
175
+ }
176
+ }
177
+
178
+ // src/modules/background.js
179
+ async function inlineBackgroundImages(source, clone, styleCache) {
180
+ const queue = [[source, clone]];
181
+ while (queue.length) {
182
+ const [srcNode, cloneNode] = queue.shift();
183
+ const style = styleCache.get(srcNode) || window.getComputedStyle(srcNode);
184
+ if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
185
+ const bg = style.getPropertyValue("background-image");
186
+ if (bg && bg.includes("url(")) {
187
+ const match = bg.match(/url\(["']?([^"')]+)["']?\)/);
188
+ if (match?.[1]) {
189
+ try {
190
+ const dataUrl = await fetchImageAsDataURL(match[1]);
191
+ cloneNode.style.backgroundImage = `url(${dataUrl})`;
192
+ } catch {
193
+ cloneNode.style.backgroundImage = "none";
194
+ }
195
+ }
196
+ }
197
+ const sChildren = Array.from(srcNode.children);
198
+ const cChildren = Array.from(cloneNode.children);
199
+ for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
200
+ queue.push([sChildren[i], cChildren[i]]);
201
+ }
202
+ await delay(1);
203
+ }
204
+ }
205
+
206
+ // src/modules/pseudo.js
207
+ async function inlinePseudoElements(source, clone) {
208
+ const importantProps = [
209
+ "position",
210
+ "top",
211
+ "right",
212
+ "bottom",
213
+ "left",
214
+ "z-index",
215
+ "width",
216
+ "height",
217
+ "min-width",
218
+ "max-width",
219
+ "min-height",
220
+ "max-height",
221
+ "margin",
222
+ "padding",
223
+ "border",
224
+ "border-radius",
225
+ "background",
226
+ "background-color",
227
+ "background-image",
228
+ "color",
229
+ "font",
230
+ "font-family",
231
+ "font-size",
232
+ "font-weight",
233
+ "text-align",
234
+ "text-decoration",
235
+ "line-height",
236
+ "letter-spacing",
237
+ "display",
238
+ "visibility",
239
+ "opacity",
240
+ "overflow",
241
+ "box-shadow",
242
+ "transform",
243
+ "transition",
244
+ "flex",
245
+ "grid"
246
+ ];
247
+ for (const pseudo of ["::before", "::after"]) {
248
+ try {
249
+ const style = window.getComputedStyle(source, pseudo);
250
+ const content = style.getPropertyValue("content");
251
+ if (content && content !== "none" && content !== '""' && content !== "''") {
252
+ const pseudoEl = document.createElement("span");
253
+ pseudoEl.className = `snapdom-pseudo-${pseudo.replace("::", "")}`;
254
+ let cleanContent = content.replace(/^\"|\"$/g, "");
255
+ if (cleanContent.startsWith("url(")) {
256
+ const match = cleanContent.match(/url\(["']?([^"')]+)["']?\)/);
257
+ if (match?.[1]) {
258
+ try {
259
+ const imgEl = document.createElement("img");
260
+ const dataUrl = await fetchImageAsDataURL(match[1]);
261
+ imgEl.src = dataUrl;
262
+ imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
263
+ pseudoEl.appendChild(imgEl);
264
+ } catch (e) {
265
+ }
266
+ }
267
+ } else {
268
+ pseudoEl.textContent = cleanContent;
269
+ }
270
+ for (const prop of importantProps) {
271
+ const value = style.getPropertyValue(prop);
272
+ if (value) {
273
+ pseudoEl.style[prop] = value;
274
+ }
275
+ }
276
+ if (!pseudoEl.style.position) pseudoEl.style.position = "absolute";
277
+ if (pseudo === "::before") {
278
+ clone.insertBefore(pseudoEl, clone.firstChild);
279
+ } else {
280
+ clone.appendChild(pseudoEl);
281
+ }
282
+ }
283
+ } catch (e) {
284
+ }
285
+ }
286
+ const sChildren = Array.from(source.children);
287
+ const cChildren = Array.from(clone.children).filter(
288
+ (child) => !child.classList.contains("snapdom-pseudo-before") && !child.classList.contains("snapdom-pseudo-after")
289
+ );
290
+ for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
291
+ await inlinePseudoElements(sChildren[i], cChildren[i]);
292
+ if (i % 5 === 0) await delay(1);
293
+ }
294
+ }
295
+
296
+ // src/utils/normalize.js
297
+ function normalizeFontName(str) {
298
+ return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
299
+ }
300
+ function normalizeVariation(value) {
301
+ const mappings = {
302
+ "normal": "400",
303
+ "bold": "700",
304
+ "italic": "italic",
305
+ "oblique": "italic",
306
+ "ultra-condensed": "50",
307
+ "extra-condensed": "62",
308
+ "condensed": "75",
309
+ "semi-condensed": "87",
310
+ "expanded": "125",
311
+ "semi-expanded": "112",
312
+ "extra-expanded": "150",
313
+ "ultra-expanded": "200"
314
+ };
315
+ return mappings[value] || value.replace(/%/g, "");
316
+ }
317
+
318
+ // src/modules/fonts.js
319
+ async function inlineFonts(styleCache = /* @__PURE__ */ new WeakMap()) {
320
+ await document.fonts.ready;
321
+ const fontFaceRules = [];
322
+ Array.from(document.styleSheets).forEach((sheet) => {
323
+ try {
324
+ Array.from(sheet.cssRules).forEach((rule) => {
325
+ if (rule instanceof CSSFontFaceRule) {
326
+ fontFaceRules.push(rule.cssText);
327
+ }
328
+ });
329
+ } catch (e) {
330
+ console.warn("Could not access stylesheet rules:", e);
331
+ }
332
+ });
333
+ const fontData = /* @__PURE__ */ new Map();
334
+ const elements = document.querySelectorAll("*");
335
+ elements.forEach((el) => {
336
+ const style = styleCache.get(el) || window.getComputedStyle(el);
337
+ if (!styleCache.has(el)) styleCache.set(el, style);
338
+ const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
339
+ if (!family) return;
340
+ const weight = normalizeVariation(style.fontWeight || "400");
341
+ const styleAttr = normalizeVariation(style.fontStyle || "normal");
342
+ const stretch = normalizeVariation(style.fontStretch || "normal");
343
+ if (!fontData.has(family)) {
344
+ fontData.set(family, /* @__PURE__ */ new Set());
345
+ }
346
+ fontData.get(family).add(`${weight}_${styleAttr}_${stretch}`);
347
+ });
348
+ const cssClasses = [...fontFaceRules];
349
+ for (const [family, variations] of fontData) {
350
+ const safeFamily = normalizeFontName(family);
351
+ for (const variation of variations) {
352
+ const [weight, styleAttr, stretch] = variation.split("_");
353
+ const variationClass = `fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`;
354
+ const cssProps = [
355
+ `font-family:"${family}",sans-serif`,
356
+ `font-weight:${weight}`,
357
+ `font-style:${styleAttr}`,
358
+ `font-stretch:${stretch}%`
359
+ ].filter(Boolean).join(";");
360
+ cssClasses.push(`.${variationClass}{${cssProps}}`);
361
+ }
362
+ }
363
+ cssClasses.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}");
364
+ return cssClasses.join("");
365
+ }
366
+ function applyFontClasses(clone, original, styleCache) {
367
+ const style = styleCache.get(original) || window.getComputedStyle(original);
368
+ const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
369
+ if (!family) return;
370
+ const weight = normalizeVariation(style.fontWeight || "400");
371
+ const styleAttr = normalizeVariation(style.fontStyle || "normal");
372
+ const stretch = normalizeVariation(style.fontStretch || "normal");
373
+ const safeFamily = normalizeFontName(family);
374
+ clone.classList.add(`fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`);
375
+ }
376
+
377
+ // src/core/prepare.js
378
+ async function prepareClone(element) {
379
+ const styleMap = /* @__PURE__ */ new Map();
380
+ const styleCache = /* @__PURE__ */ new WeakMap();
381
+ const nodeMap = /* @__PURE__ */ new WeakMap();
382
+ let defaults;
383
+ try {
384
+ defaults = getDefaultStyles();
385
+ } catch (e) {
386
+ console.warn("getDefaultStyles failed:", e);
387
+ defaults = {};
388
+ }
389
+ let clone;
390
+ try {
391
+ clone = deepCloneWithShadow(element, styleMap, defaults, styleCache, nodeMap);
392
+ } catch (e) {
393
+ console.warn("deepCloneWithShadow failed:", e);
394
+ throw e;
395
+ }
396
+ try {
397
+ await inlineImages(clone);
398
+ } catch (e) {
399
+ console.warn("inlineImages failed:", e);
400
+ }
401
+ try {
402
+ await inlineBackgroundImages(element, clone, styleCache);
403
+ } catch (e) {
404
+ console.warn("inlineBackgroundImages failed:", e);
405
+ }
406
+ try {
407
+ await inlinePseudoElements(element, clone);
408
+ } catch (e) {
409
+ console.warn("inlinePseudoElements failed:", e);
410
+ }
411
+ const keyToClass = generateReusableCSSClasses(styleMap);
412
+ const classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
413
+ for (const [node, key] of styleMap.entries()) {
414
+ if (node.tagName === "STYLE") continue;
415
+ const className = keyToClass.get(key);
416
+ if (className) {
417
+ node.classList.add(className);
418
+ try {
419
+ applyFontClasses(node, nodeMap.get(node), styleCache);
420
+ } catch (e) {
421
+ console.warn("applyFontClasses failed for node:", node, e);
422
+ }
423
+ }
424
+ const bgImage = node.style?.backgroundImage;
425
+ node.removeAttribute("style");
426
+ if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
427
+ }
428
+ return { clone, classCSS, styleCache };
429
+ }
430
+
431
+ // src/utils/minifySVG.js
432
+ function minifySVG(svgString) {
433
+ return svgString.replace(/>\s+</g, "><").replace(/\s{2,}/g, " ").replace(/\n/g, "").trim();
434
+ }
435
+
436
+ // src/core/capture.js
437
+ async function capture(element, scale = 1) {
438
+ let clone, classCSS, styleCache;
439
+ try {
440
+ ({ clone, classCSS, styleCache } = await prepareClone(element));
441
+ } catch (e) {
442
+ console.error("prepareClone failed:", e);
443
+ throw e;
444
+ }
445
+ const w = element.offsetWidth * scale;
446
+ const h = element.offsetHeight * scale;
447
+ let fonts = "";
448
+ try {
449
+ fonts = await inlineFonts(styleCache);
450
+ } catch (e) {
451
+ console.warn("inlineFonts failed:", e);
452
+ }
453
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
454
+ svg.setAttribute("width", w);
455
+ svg.setAttribute("height", h);
456
+ svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
457
+ const fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
458
+ fo.setAttribute("width", "100%");
459
+ fo.setAttribute("height", "100%");
460
+ const div = document.createElement("div");
461
+ div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
462
+ div.setAttribute("style", `width:${w}px;height:${h}px;`);
463
+ const styleTag = document.createElement("style");
464
+ styleTag.textContent = fonts + "svg{overflow:visible}" + classCSS;
465
+ div.appendChild(styleTag);
466
+ div.appendChild(clone);
467
+ fo.appendChild(div);
468
+ svg.appendChild(fo);
469
+ try {
470
+ const svgStr = new XMLSerializer().serializeToString(svg);
471
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(minifySVG(svgStr))}`;
472
+ } catch (e) {
473
+ console.error("SVG serialization failed:", e);
474
+ throw e;
475
+ }
476
+ }
477
+
478
+ // src/api/snapdom.js
479
+ async function snapdom(el, scale = 1) {
480
+ return await capture(el, scale);
481
+ }
482
+ snapdom.toImg = async function(el, scale = 1) {
483
+ const dataURL = await snapdom(el, scale);
484
+ const img = new Image();
485
+ img.src = dataURL;
486
+ await img.decode();
487
+ return img;
488
+ };
489
+ snapdom.toCanvas = async function(el, scale = 1) {
490
+ const img = await snapdom.toImg(el, scale);
491
+ const canvas = document.createElement("canvas");
492
+ canvas.width = img.width;
493
+ canvas.height = img.height;
494
+ const ctx = canvas.getContext("2d");
495
+ ctx.drawImage(img, 0, 0);
496
+ return canvas;
497
+ };
498
+ snapdom.toPng = async function(el, scale = 1) {
499
+ const canvas = await snapdom.toCanvas(el, scale);
500
+ const img = new Image();
501
+ img.src = canvas.toDataURL("image/png");
502
+ await img.decode();
503
+ return img;
504
+ };
505
+ snapdom.toJpg = async function(el, scale = 1, quality = 0.92) {
506
+ const canvas = await snapdom.toCanvas(el, scale);
507
+ const img = new Image();
508
+ img.src = canvas.toDataURL("image/jpeg", quality);
509
+ await img.decode();
510
+ return img;
511
+ };
512
+ snapdom.toWebp = async function(el, scale = 1, quality = 0.92) {
513
+ const canvas = await snapdom.toCanvas(el, scale);
514
+ const img = new Image();
515
+ img.src = canvas.toDataURL("image/webp", quality);
516
+ await img.decode();
517
+ return img;
518
+ };
519
+ snapdom.toBlob = async function(el, scale = 1) {
520
+ const dataURL = await snapdom(el, scale);
521
+ const svgXml = decodeURIComponent(dataURL.split(",")[1]);
522
+ return new Blob([svgXml], { type: "image/svg+xml" });
523
+ };
524
+ export {
525
+ snapdom
526
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@zumer/snapdom",
3
+ "version": "0.9.0",
4
+ "description": "snapDOM captures DOM elements as images with exceptional speed and minimal overhead. Designed to avoid bottlenecks and long tasks.",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "npx vitest run --browser.headless --reporter=verbose",
8
+ "test:benchmark": "npx vitest bench --browser.headless --watch=false",
9
+ "compile-browser": "npx @zumerbox/build -n snapdom -j src/index.js",
10
+ "compile-esm": "npx @zumerbox/build -n snapdom -j src/index.js -p neutral -x",
11
+ "compile": "npm run compile-browser && npm run compile-esm",
12
+ "build": "npm run compile && npm pack",
13
+ "bump": "npx @zumerbox/bump && npx @zumerbox/changelog",
14
+ "prebuild": "git add CHANGELOG.md && git commit -m \"Bumped version\" && git push --follow-tags"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/zumerlab/snapdom.git"
19
+ },
20
+ "files": [
21
+ "dist/snapdom.js",
22
+ "dist/snapdom.min.js",
23
+ "dist/snapdom.min.mjs",
24
+ "dist/snapdom.mjs",
25
+ "README.md"
26
+ ],
27
+ "keywords": [
28
+ "zumerlab",
29
+ "snapDOM",
30
+ "screenshot",
31
+ "engine",
32
+ "html capture",
33
+ "dom capture",
34
+ "html to image",
35
+ "dom to image",
36
+ "html screenshot",
37
+ "capture element",
38
+ "html snapshot",
39
+ "element screenshot",
40
+ "web capture",
41
+ "snapshot tool",
42
+ "render html",
43
+ "capture dom",
44
+ "web snapshot",
45
+ "html export",
46
+ "dom snapshot",
47
+ "html to png",
48
+ "html to svg"
49
+ ],
50
+ "author": "Juan Martin Muda",
51
+ "license": "MIT",
52
+ "bugs": {
53
+ "url": "https://github.com/zumerlab/snapdom/issues"
54
+ },
55
+ "homepage": "https://github.com/zumerlab/snapdom#readme",
56
+ "devDependencies": {
57
+ "@vitest/browser": "^3.1.2",
58
+ "playwright": "^1.52.0"
59
+ }
60
+ }
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @file snapdom.js
3
+ */
4
+
5
+ export { snapdom } from './api/snapdom';
6
+