@zumer/snapdom 1.2.5 → 1.7.1
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 +36 -9
- package/dist/snapdom.js +164 -46
- package/dist/snapdom.min.js +15 -3
- package/dist/snapdom.min.mjs +15 -3
- package/dist/snapdom.mjs +164 -46
- package/package.json +2 -2
- package/types/snapdom.d.ts +2 -0
package/README.md
CHANGED
|
@@ -122,14 +122,34 @@ Returns an object with reusable export methods:
|
|
|
122
122
|
|
|
123
123
|
All capture methods accept an `options` object:
|
|
124
124
|
|
|
125
|
-
| Option | Type
|
|
126
|
-
| ----------------- |
|
|
127
|
-
| `compress` | boolean
|
|
128
|
-
| `fast` | boolean
|
|
129
|
-
| `embedFonts` | boolean
|
|
130
|
-
| `scale` | number
|
|
131
|
-
| `backgroundColor` | string
|
|
132
|
-
| `quality` | number
|
|
125
|
+
| Option | Type | Default | Description |
|
|
126
|
+
| ----------------- | -------- | -------- | ------------------------------------------ |
|
|
127
|
+
| `compress` | boolean | `true` | Removes redundant styles |
|
|
128
|
+
| `fast` | boolean | `true` | Skips idle delay for faster results |
|
|
129
|
+
| `embedFonts` | boolean | `false` | Inlines fonts (icon fonts always embedded) |
|
|
130
|
+
| `scale` | number | `1` | Output scale multiplier |
|
|
131
|
+
| `backgroundColor` | string | `"#fff"` | Fallback color for JPG/WebP |
|
|
132
|
+
| `quality` | number | `1` | Quality for JPG/WebP (0 to 1) |
|
|
133
|
+
| `crossOrigin` | function | - | Function to determine CORS mode per image URL |
|
|
134
|
+
|
|
135
|
+
### Cross-Origin Images
|
|
136
|
+
|
|
137
|
+
By default, snapDOM loads images with `crossOrigin="anonymous"`. You can customize this behavior using the `crossOrigin` option:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const result = await snapdom(element, {
|
|
141
|
+
crossOrigin: (url) => {
|
|
142
|
+
// Use credentials for same-origin images
|
|
143
|
+
if (url.startsWith(window.location.origin)) {
|
|
144
|
+
return "use-credentials";
|
|
145
|
+
}
|
|
146
|
+
// Use anonymous for cross-origin images
|
|
147
|
+
return "anonymous";
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This is useful when your images require authentication or when dealing with credentialed requests.
|
|
133
153
|
|
|
134
154
|
### Download options
|
|
135
155
|
|
|
@@ -164,6 +184,7 @@ import { snapdom, preCache } from './snapdom.mjs';
|
|
|
164
184
|
|
|
165
185
|
* `embedFonts` *(boolean, default: true)* — Inlines non-icon fonts during preload.
|
|
166
186
|
* `reset` *(boolean, default: false)* — Clears all existing internal caches.
|
|
187
|
+
* `crossOrigin` *(function)* — Function to determine CORS mode per image URL during preload.
|
|
167
188
|
|
|
168
189
|
|
|
169
190
|
## Features
|
|
@@ -177,9 +198,10 @@ import { snapdom, preCache } from './snapdom.mjs';
|
|
|
177
198
|
|
|
178
199
|
## Limitations
|
|
179
200
|
|
|
180
|
-
* External images must be CORS-accessible
|
|
201
|
+
* External images must be CORS-accessible (use `crossOrigin` option for credentialed requests)
|
|
181
202
|
* Iframes are not supported
|
|
182
203
|
* When WebP format is used on Safari, it will fallback to PNG rendering.
|
|
204
|
+
* `@font-face` CSS rule is well supported, but if need to use JS `FontFace()`, see this workaround [`#43`](https://github.com/zumerlab/snapdom/issues/43)
|
|
183
205
|
|
|
184
206
|
## Benchmarks
|
|
185
207
|
|
|
@@ -215,6 +237,11 @@ npm run test:benchmark
|
|
|
215
237
|
|
|
216
238
|
They execute in **headless Chromium** using real DOM nodes.
|
|
217
239
|
|
|
240
|
+
## Contributors 🙌
|
|
241
|
+
|
|
242
|
+
<a href="https://github.com/zumerlab/snapdom/graphs/contributors">
|
|
243
|
+
<img src="https://contrib.rocks/image?repo=zumerlab/snapdom" />
|
|
244
|
+
</a>
|
|
218
245
|
|
|
219
246
|
## License
|
|
220
247
|
|
package/dist/snapdom.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.
|
|
4
|
+
* v.1.7.1
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
];
|
|
210
210
|
return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
|
|
211
211
|
}
|
|
212
|
-
function fetchImage(src, timeout = 3e3) {
|
|
212
|
+
function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
213
213
|
if (imageCache.has(src)) {
|
|
214
214
|
return Promise.resolve(imageCache.get(src));
|
|
215
215
|
}
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
reject(new Error("Image load timed out"));
|
|
219
219
|
}, timeout);
|
|
220
220
|
const image = new Image();
|
|
221
|
-
image.crossOrigin =
|
|
221
|
+
image.crossOrigin = crossOrigin;
|
|
222
222
|
image.onload = async () => {
|
|
223
223
|
clearTimeout(timeoutId);
|
|
224
224
|
try {
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
canvas.width = image.width;
|
|
228
228
|
canvas.height = image.height;
|
|
229
229
|
const ctx = canvas.getContext("2d");
|
|
230
|
-
ctx.drawImage(image, 0, 0);
|
|
230
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
231
231
|
try {
|
|
232
232
|
const dataURL = canvas.toDataURL("image/png");
|
|
233
233
|
imageCache.set(src, dataURL);
|
|
@@ -315,6 +315,26 @@
|
|
|
315
315
|
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
316
316
|
const clone = node.cloneNode(false);
|
|
317
317
|
nodeMap.set(clone, node);
|
|
318
|
+
if (node instanceof HTMLInputElement) {
|
|
319
|
+
clone.value = node.value;
|
|
320
|
+
clone.setAttribute("value", node.value);
|
|
321
|
+
if (node.checked !== void 0) {
|
|
322
|
+
clone.checked = node.checked;
|
|
323
|
+
if (node.checked) clone.setAttribute("checked", "");
|
|
324
|
+
}
|
|
325
|
+
} else if (node instanceof HTMLTextAreaElement) {
|
|
326
|
+
clone.value = node.value;
|
|
327
|
+
clone.textContent = node.value;
|
|
328
|
+
} else if (node instanceof HTMLSelectElement) {
|
|
329
|
+
clone.value = node.value;
|
|
330
|
+
Array.from(clone.options).forEach((opt) => {
|
|
331
|
+
if (opt.value === node.value) {
|
|
332
|
+
opt.setAttribute("selected", "");
|
|
333
|
+
} else {
|
|
334
|
+
opt.removeAttribute("selected");
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
318
338
|
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
319
339
|
const frag = document.createDocumentFragment();
|
|
320
340
|
node.childNodes.forEach((child) => {
|
|
@@ -355,7 +375,7 @@
|
|
|
355
375
|
ctx.fillText(unicodeChar, 0, ascent);
|
|
356
376
|
return canvas.toDataURL();
|
|
357
377
|
}
|
|
358
|
-
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
378
|
+
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
|
|
359
379
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
|
|
360
380
|
let finalCSS = "";
|
|
361
381
|
for (const link of links) {
|
|
@@ -396,7 +416,7 @@
|
|
|
396
416
|
resourceCache.set(url, b64);
|
|
397
417
|
return { original: match[0], inlined: `url(${b64})` };
|
|
398
418
|
} catch (err) {
|
|
399
|
-
console.warn("
|
|
419
|
+
console.warn("[snapdom] Failed to fetch font:", url);
|
|
400
420
|
return null;
|
|
401
421
|
}
|
|
402
422
|
})
|
|
@@ -409,7 +429,90 @@
|
|
|
409
429
|
}
|
|
410
430
|
finalCSS += cssFinal + "\n";
|
|
411
431
|
} catch (e) {
|
|
412
|
-
console.warn("
|
|
432
|
+
console.warn("[snapdom] Failed to fetch CSS:", link.href);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const sheet of document.styleSheets) {
|
|
436
|
+
try {
|
|
437
|
+
if (!sheet.href || links.every((link) => link.href !== sheet.href)) {
|
|
438
|
+
for (const rule of sheet.cssRules) {
|
|
439
|
+
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
440
|
+
const src = rule.style.getPropertyValue("src");
|
|
441
|
+
if (!src) continue;
|
|
442
|
+
const urlRegex = /url\(([^)]+)\)/g;
|
|
443
|
+
let inlinedSrc = src;
|
|
444
|
+
const matches = Array.from(src.matchAll(urlRegex));
|
|
445
|
+
for (const match of matches) {
|
|
446
|
+
let rawUrl = match[1].trim().replace(/^["']|["']$/g, "");
|
|
447
|
+
if (!rawUrl) continue;
|
|
448
|
+
let url = rawUrl;
|
|
449
|
+
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
450
|
+
url = new URL(url, sheet.href || location.href).href;
|
|
451
|
+
}
|
|
452
|
+
if (ignoreIconFonts && isIconFont(url)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (resourceCache.has(url)) {
|
|
456
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const res = await fetch(url);
|
|
461
|
+
const blob = await res.blob();
|
|
462
|
+
const b64 = await new Promise((resolve) => {
|
|
463
|
+
const reader = new FileReader();
|
|
464
|
+
reader.onload = () => resolve(reader.result);
|
|
465
|
+
reader.readAsDataURL(blob);
|
|
466
|
+
});
|
|
467
|
+
resourceCache.set(url, b64);
|
|
468
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finalCSS += `@font-face {
|
|
474
|
+
font-family: ${rule.style.getPropertyValue("font-family")};
|
|
475
|
+
src: ${inlinedSrc};
|
|
476
|
+
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
477
|
+
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
478
|
+
}
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.warn("[snapdom] Cannot access stylesheet", sheet.href, e);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const font of document.fonts) {
|
|
488
|
+
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
489
|
+
let b64 = font._snapdomSrc;
|
|
490
|
+
if (!b64.startsWith("data:")) {
|
|
491
|
+
if (resourceCache.has(font._snapdomSrc)) {
|
|
492
|
+
b64 = resourceCache.get(font._snapdomSrc);
|
|
493
|
+
} else {
|
|
494
|
+
try {
|
|
495
|
+
const res = await fetch(font._snapdomSrc);
|
|
496
|
+
const blob = await res.blob();
|
|
497
|
+
b64 = await new Promise((resolve) => {
|
|
498
|
+
const reader = new FileReader();
|
|
499
|
+
reader.onload = () => resolve(reader.result);
|
|
500
|
+
reader.readAsDataURL(blob);
|
|
501
|
+
});
|
|
502
|
+
resourceCache.set(font._snapdomSrc, b64);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
finalCSS += `@font-face {
|
|
510
|
+
font-family: '${font.family}';
|
|
511
|
+
src: url(${b64});
|
|
512
|
+
font-style: ${font.style || "normal"};
|
|
513
|
+
font-weight: ${font.weight || "normal"};
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
413
516
|
}
|
|
414
517
|
}
|
|
415
518
|
if (finalCSS && preCached) {
|
|
@@ -467,18 +570,25 @@
|
|
|
467
570
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
468
571
|
pseudoEl.appendChild(imgEl);
|
|
469
572
|
} catch (e) {
|
|
470
|
-
console.error(
|
|
471
|
-
`[snapdom] Error in pseudo ${pseudo} for`,
|
|
472
|
-
source,
|
|
473
|
-
e
|
|
474
|
-
);
|
|
573
|
+
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
475
574
|
}
|
|
476
575
|
}
|
|
477
576
|
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
478
577
|
pseudoEl.textContent = cleanContent;
|
|
479
578
|
}
|
|
579
|
+
if (hasBg && bg) {
|
|
580
|
+
const rawUrl = extractURL(bg);
|
|
581
|
+
if (rawUrl) {
|
|
582
|
+
try {
|
|
583
|
+
const dataUrl = await fetchImage(encodeURI(rawUrl));
|
|
584
|
+
pseudoEl.style.backgroundImage = `url(${dataUrl})`;
|
|
585
|
+
} catch (e) {
|
|
586
|
+
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
480
590
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
|
|
481
|
-
if (!hasContent2) continue;
|
|
591
|
+
if (!hasContent2 && !hasBg) continue;
|
|
482
592
|
if (pseudo === "::before") {
|
|
483
593
|
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
484
594
|
} else {
|
|
@@ -490,9 +600,7 @@
|
|
|
490
600
|
}
|
|
491
601
|
}
|
|
492
602
|
const sChildren = Array.from(source.children);
|
|
493
|
-
const cChildren = Array.from(clone.children).filter(
|
|
494
|
-
(child) => !child.dataset.snapdomPseudo
|
|
495
|
-
);
|
|
603
|
+
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
496
604
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
497
605
|
await inlinePseudoElements(
|
|
498
606
|
sChildren[i],
|
|
@@ -563,12 +671,13 @@
|
|
|
563
671
|
}
|
|
564
672
|
|
|
565
673
|
// src/modules/images.js
|
|
566
|
-
async function inlineImages(clone) {
|
|
674
|
+
async function inlineImages(clone, options = {}) {
|
|
567
675
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
568
676
|
const processImg = async (img) => {
|
|
569
677
|
const src = img.src;
|
|
570
678
|
try {
|
|
571
|
-
const
|
|
679
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
|
|
680
|
+
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
572
681
|
img.src = dataUrl;
|
|
573
682
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
574
683
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -586,29 +695,36 @@
|
|
|
586
695
|
}
|
|
587
696
|
|
|
588
697
|
// src/modules/background.js
|
|
589
|
-
async function inlineBackgroundImages(source, clone, styleCache) {
|
|
698
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
590
699
|
const queue = [[source, clone]];
|
|
591
700
|
while (queue.length) {
|
|
592
701
|
const [srcNode, cloneNode] = queue.shift();
|
|
593
702
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
594
703
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
595
704
|
const bg = style.getPropertyValue("background-image");
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
dataUrl
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
705
|
+
const bgSplits = bg.split(",");
|
|
706
|
+
for (let i = 0; i < bgSplits.length; i++) {
|
|
707
|
+
const rawUrl = extractURL(bgSplits[i]);
|
|
708
|
+
if (rawUrl) {
|
|
709
|
+
try {
|
|
710
|
+
let bgUrl = encodeURI(rawUrl);
|
|
711
|
+
let dataUrl;
|
|
712
|
+
if (bgCache.has(bgUrl)) {
|
|
713
|
+
dataUrl = bgCache.get(bgUrl);
|
|
714
|
+
} else {
|
|
715
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(bgUrl) : "anonymous";
|
|
716
|
+
dataUrl = await fetchImage(bgUrl, 3e3, crossOrigin);
|
|
717
|
+
bgCache.set(bgUrl, dataUrl);
|
|
718
|
+
}
|
|
719
|
+
bgSplits[i] = `url(${dataUrl})`;
|
|
720
|
+
} catch {
|
|
721
|
+
bgSplits[i] = "none";
|
|
606
722
|
}
|
|
607
|
-
cloneNode.style.backgroundImage = `url(${dataUrl})`;
|
|
608
|
-
} catch {
|
|
609
|
-
cloneNode.style.backgroundImage = "none";
|
|
610
723
|
}
|
|
611
724
|
}
|
|
725
|
+
if (bgSplits.some((s) => s.includes("url("))) {
|
|
726
|
+
cloneNode.style.backgroundImage = bgSplits.join(",");
|
|
727
|
+
}
|
|
612
728
|
const sChildren = Array.from(srcNode.children);
|
|
613
729
|
const cChildren = Array.from(cloneNode.children);
|
|
614
730
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
@@ -629,13 +745,13 @@
|
|
|
629
745
|
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
630
746
|
await new Promise((resolve) => {
|
|
631
747
|
idle(async () => {
|
|
632
|
-
await inlineImages(clone);
|
|
748
|
+
await inlineImages(clone, options);
|
|
633
749
|
resolve();
|
|
634
750
|
}, { fast });
|
|
635
751
|
});
|
|
636
752
|
await new Promise((resolve) => {
|
|
637
753
|
idle(async () => {
|
|
638
|
-
await inlineBackgroundImages(element, clone, styleCache);
|
|
754
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
639
755
|
resolve();
|
|
640
756
|
}, { fast });
|
|
641
757
|
});
|
|
@@ -708,20 +824,20 @@
|
|
|
708
824
|
}
|
|
709
825
|
return img;
|
|
710
826
|
}
|
|
711
|
-
async function toCanvas(url, { dpr = 1, scale = 1 }) {
|
|
827
|
+
async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
712
828
|
const img = new Image();
|
|
713
829
|
img.src = url;
|
|
714
830
|
await img.decode();
|
|
715
831
|
const canvas = document.createElement("canvas");
|
|
716
|
-
const width = img.width * scale
|
|
717
|
-
const height = img.height * scale
|
|
718
|
-
canvas.width = Math.ceil(width);
|
|
719
|
-
canvas.height = Math.ceil(height);
|
|
832
|
+
const width = img.width * scale;
|
|
833
|
+
const height = img.height * scale;
|
|
834
|
+
canvas.width = Math.ceil(width * dpr);
|
|
835
|
+
canvas.height = Math.ceil(height * dpr);
|
|
720
836
|
const ctx = canvas.getContext("2d");
|
|
721
|
-
ctx.scale(
|
|
722
|
-
ctx.drawImage(img, 0, 0);
|
|
723
|
-
canvas.style.width = `${
|
|
724
|
-
canvas.style.height = `${
|
|
837
|
+
ctx.scale(dpr, dpr);
|
|
838
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
839
|
+
canvas.style.width = `${width}px`;
|
|
840
|
+
canvas.style.height = `${height}px`;
|
|
725
841
|
return canvas;
|
|
726
842
|
}
|
|
727
843
|
async function toBlob(url) {
|
|
@@ -817,7 +933,7 @@
|
|
|
817
933
|
|
|
818
934
|
// src/api/preCache.js
|
|
819
935
|
async function preCache(root = document, options = {}) {
|
|
820
|
-
const { embedFonts = true, reset = false,
|
|
936
|
+
const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
|
|
821
937
|
if (reset) {
|
|
822
938
|
imageCache.clear();
|
|
823
939
|
bgCache.clear();
|
|
@@ -837,8 +953,9 @@
|
|
|
837
953
|
for (const img of imgEls) {
|
|
838
954
|
const src = img.src;
|
|
839
955
|
if (!imageCache.has(src)) {
|
|
956
|
+
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
840
957
|
promises.push(
|
|
841
|
-
fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
958
|
+
fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
842
959
|
})
|
|
843
960
|
);
|
|
844
961
|
}
|
|
@@ -847,8 +964,9 @@
|
|
|
847
964
|
const bg = getComputedStyle(el).backgroundImage;
|
|
848
965
|
const url = extractURL(bg);
|
|
849
966
|
if (url && !bgCache.has(url)) {
|
|
967
|
+
const crossOrigin = crossOriginFn ? crossOriginFn(url) : "anonymous";
|
|
850
968
|
promises.push(
|
|
851
|
-
fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
969
|
+
fetchImage(url, 3e3, crossOrigin).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
852
970
|
})
|
|
853
971
|
);
|
|
854
972
|
}
|
package/dist/snapdom.min.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
(()=>{var
|
|
2
|
-
`;return o}function J(
|
|
3
|
-
`}catch{console.warn("
|
|
1
|
+
(()=>{var R=new Map,T=new Map,x=new Map,I=new Map,k=new Map,N=new WeakMap;var ce=["div","span","p","a","img","ul","li","button","input","select","textarea","label","section","article","header","footer","nav","main","aside","h1","h2","h3","h4","h5","h6","svg","path","circle","rect","line","g","table","thead","tbody","tr","td","th"];function H(){for(let e of ce)z(e)}function z(e){if(I.has(e))return I.get(e);if(new Set(["script","style","meta","link","noscript","template"]).has(e)){let s={};return I.set(e,s),s}let o=document.getElementById("snapdom-sandbox");o||(o=document.createElement("div"),o.id="snapdom-sandbox",o.style.position="absolute",o.style.left="-9999px",o.style.top="-9999px",o.style.width="0",o.style.height="0",o.style.overflow="hidden",document.body.appendChild(o));let r=document.createElement(e);r.style.all="initial",o.appendChild(r);let t=getComputedStyle(r),a={};for(let s of t)a[s]=t.getPropertyValue(s);return o.removeChild(r),I.set(e,a),a}function M(e,n,o=!1){let r=[],t=z(n);for(let[a,s]of Object.entries(e))if(!o)s&&r.push(`${a}:${s}`);else{let i=t[a];s&&s!==i&&r.push(`${a}:${s}`)}return r.sort().join(";")}function Y(e){let n=new Set;return e.nodeType!==Node.ELEMENT_NODE&&e.nodeType!==Node.DOCUMENT_FRAGMENT_NODE?[]:(e.tagName&&n.add(e.tagName.toLowerCase()),typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(o=>n.add(o.tagName.toLowerCase())),Array.from(n))}function K(e){let n=new Map;for(let r of e){let t=I.get(r);if(!t)continue;let a=Object.entries(t).map(([s,i])=>`${s}:${i};`).sort().join("");n.has(a)||n.set(a,[]),n.get(a).push(r)}let o="";for(let[r,t]of n.entries())o+=`${t.join(",")} { ${r} }
|
|
2
|
+
`;return o}function J(e){let n=new Set(e.values()),o=new Map,r=1;for(let t of n)o.set(t,`c${r++}`);return o}function L(e,{fast:n=!1}={}){if(n)return e();"requestIdleCallback"in window?requestIdleCallback(e,{timeout:50}):setTimeout(e,1)}function U(e,n=null){if(!(e instanceof Element))return window.getComputedStyle(e,n);let o=N.get(e);if(o||(o=new Map,N.set(e,o)),!o.has(n)){let r=window.getComputedStyle(e,n);o.set(n,r)}return o.get(n)}function X(e){let n=e.replace(/^['"]|['"]$/g,"");if(n.startsWith("\\"))try{return String.fromCharCode(parseInt(n.replace("\\",""),16))}catch{return n}return n}function $(e){let n=e.indexOf("url(");if(n===-1)return null;let o=e.slice(n+4).trim();return o.endsWith(")")&&(o=o.slice(0,-1).trim()),(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),o}function P(e){return[/font\s*awesome/i,/material\s*icons/i,/ionicons/i,/glyphicons/i,/feather/i,/bootstrap\s*icons/i,/remix\s*icons/i,/heroicons/i].some(o=>o.test(e))}function S(e,n=3e3,o="anonymous"){return R.has(e)?Promise.resolve(R.get(e)):new Promise((r,t)=>{let a=setTimeout(()=>{t(new Error("Image load timed out"))},n),s=new Image;s.crossOrigin=o,s.onload=async()=>{clearTimeout(a);try{await s.decode();let i=document.createElement("canvas");i.width=s.width,i.height=s.height,i.getContext("2d").drawImage(s,0,0,i.width,i.height);try{let l=i.toDataURL("image/png");R.set(e,l),r(l)}catch{t(new Error("CORS restrictions prevented image capture"))}}catch(i){t(i)}},s.onerror=i=>{clearTimeout(a),t(new Error("Failed to load image: "+(i.message||"Unknown error")))},s.src=e})}function O(e){let n={};for(let o of e)n[o]=e.getPropertyValue(o);return n}function B(){return/^((?!chrome|android).)*safari/i.test(navigator.userAgent)}function _(e,n,o,r,t){if(e.tagName==="STYLE")return;r.has(e)||r.set(e,U(e));let a=r.get(e),s=O(a),i=e.tagName?.toLowerCase()||"div",c=M(s,i,t);o.set(n,c)}function D(e,n,o,r,t){if(e.nodeType===Node.ELEMENT_NODE&&e.getAttribute("data-capture")==="exclude"){let i=document.createElement("div"),c=e.getBoundingClientRect();return i.style.cssText=`display: inline-block; width: ${c.width}px; height: ${c.height}px; visibility: hidden;`,i}if(e.tagName==="IFRAME"){let i=document.createElement("div");return i.textContent="",i.style.cssText=`width: ${e.offsetWidth}px; height: ${e.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;`,i}if(e.nodeType===Node.ELEMENT_NODE&&e.getAttribute("data-capture")==="placeholder"){let i=e.cloneNode(!1);r.set(i,e),_(e,i,n,o,t);let c=document.createElement("div");return c.textContent=e.getAttribute("data-placeholder-text")||"",c.style.cssText="color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;",i.appendChild(c),i}if(e.tagName==="CANVAS"){let i=e.toDataURL(),c=document.createElement("img");return c.src=i,c.width=e.width,c.height=e.height,c.style.display="inline-block",c.style.width=`${e.width}px`,c.style.height=`${e.height}px`,c}if(e.nodeType===Node.TEXT_NODE){if(e.parentElement?.shadowRoot){let i=e.parentElement.tagName.toLowerCase();if(customElements.get(i))return null}return e.cloneNode(!0)}if(e.nodeType!==Node.ELEMENT_NODE)return e.cloneNode(!0);let a=e.cloneNode(!1);r.set(a,e),e instanceof HTMLInputElement?(a.value=e.value,a.setAttribute("value",e.value),e.checked!==void 0&&(a.checked=e.checked,e.checked&&a.setAttribute("checked",""))):e instanceof HTMLTextAreaElement?(a.value=e.value,a.textContent=e.value):e instanceof HTMLSelectElement&&(a.value=e.value,Array.from(a.options).forEach(i=>{i.value===e.value?i.setAttribute("selected",""):i.removeAttribute("selected")})),_(e,a,n,o,t);let s=document.createDocumentFragment();if(e.childNodes.forEach(i=>{let c=D(i,n,o,r,t);c&&s.appendChild(c)}),a.appendChild(s),e.shadowRoot){let i=Array.from(e.shadowRoot.children).filter(l=>l.tagName!=="STYLE").map(l=>D(l,n,o,r)).filter(Boolean),c=document.createDocumentFragment();i.forEach(l=>c.appendChild(l)),a.appendChild(c)}return a}async function G(e,n,o,r=32,t="#000"){n=n.replace(/^['"]+|['"]+$/g,"");let a=window.devicePixelRatio||1,i=document.createElement("canvas").getContext("2d");i.font=o?`${o} ${r}px "${n}"`:`${r}px "${n}"`;let c=i.measureText(e),l=c.actualBoundingBoxAscent||r*.8,d=c.actualBoundingBoxDescent||r*.2,h=l+d,f=c.width,p=document.createElement("canvas");p.width=Math.ceil(f*a),p.height=Math.ceil(h*a);let m=p.getContext("2d");return m.scale(a,a),m.font=i.font,m.textAlign="left",m.textBaseline="alphabetic",m.fillStyle=t,m.fillText(e,0,l),p.toDataURL()}async function W({ignoreIconFonts:e=!0,preCached:n=!1}={}){let o=Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(t=>t.href),r="";for(let t of o)try{let s=await(await fetch(t.href)).text();if(e&&(P(t.href)||P(s)))continue;let i=/url\(([^)]+)\)/g,c=await Promise.all(Array.from(s.matchAll(i)).map(async d=>{let h=$(d[0]);if(!h)return null;let f=h;f.startsWith("http")||(f=new URL(f,t.href).href);try{f=encodeURI(f)}catch{console.warn("[snapdom] Failed to encode font URL:",f)}if(e&&P(f))return null;if(x.has(f))return{original:d[0],inlined:`url(${x.get(f)})`};try{let m=await(await fetch(f)).blob(),g=await new Promise(y=>{let C=new FileReader;C.onload=()=>y(C.result),C.readAsDataURL(m)});return x.set(f,g),{original:d[0],inlined:`url(${g})`}}catch{return console.warn("[snapdom] Failed to fetch font:",f),null}})),l=s;for(let d of c)d&&(l=l.replace(d.original,d.inlined));r+=l+`
|
|
3
|
+
`}catch{console.warn("[snapdom] Failed to fetch CSS:",t.href)}for(let t of document.styleSheets)try{if(!t.href||o.every(a=>a.href!==t.href)){for(let a of t.cssRules)if(a.type===CSSRule.FONT_FACE_RULE){let s=a.style.getPropertyValue("src");if(!s)continue;let i=/url\(([^)]+)\)/g,c=s,l=Array.from(s.matchAll(i));for(let d of l){let h=d[1].trim().replace(/^["']|["']$/g,"");if(!h)continue;let f=h;if(!f.startsWith("http")&&!f.startsWith("data:")&&(f=new URL(f,t.href||location.href).href),!(e&&P(f))){if(x.has(f)){c=c.replace(d[0],`url(${x.get(f)})`);continue}try{let m=await(await fetch(f)).blob(),g=await new Promise(y=>{let C=new FileReader;C.onload=()=>y(C.result),C.readAsDataURL(m)});x.set(f,g),c=c.replace(d[0],`url(${g})`)}catch{console.warn("[snapdom] Failed to fetch font URL:",f)}}}r+=`@font-face {
|
|
4
|
+
font-family: ${a.style.getPropertyValue("font-family")};
|
|
5
|
+
src: ${c};
|
|
6
|
+
font-style: ${a.style.getPropertyValue("font-style")||"normal"};
|
|
7
|
+
font-weight: ${a.style.getPropertyValue("font-weight")||"normal"};
|
|
8
|
+
}
|
|
9
|
+
`}}}catch(a){console.warn("[snapdom] Cannot access stylesheet",t.href,a)}for(let t of document.fonts)if(t.family&&t.status==="loaded"&&t._snapdomSrc){let a=t._snapdomSrc;if(!a.startsWith("data:"))if(x.has(t._snapdomSrc))a=x.get(t._snapdomSrc);else try{let i=await(await fetch(t._snapdomSrc)).blob();a=await new Promise(c=>{let l=new FileReader;l.onload=()=>c(l.result),l.readAsDataURL(i)}),x.set(t._snapdomSrc,a)}catch{console.warn("[snapdom] Failed to fetch dynamic font src:",t._snapdomSrc);continue}r+=`@font-face {
|
|
10
|
+
font-family: '${t.family}';
|
|
11
|
+
src: url(${a});
|
|
12
|
+
font-style: ${t.style||"normal"};
|
|
13
|
+
font-weight: ${t.weight||"normal"};
|
|
14
|
+
}
|
|
15
|
+
`}if(r&&n){let t=document.createElement("style");t.setAttribute("data-snapdom","embedFonts"),t.textContent=r,document.head.appendChild(t)}return r}async function V(e,n,o,r,t,a=!1){if(!(e instanceof Element)||!(n instanceof Element))return;for(let c of["::before","::after"])try{let l=U(e,c);if(!l)continue;let d=l.getPropertyValue("content"),h=l.getPropertyValue("background-image"),f=d&&d!=="none"&&d!=='""'&&d!=="''",p=h&&h.startsWith("url(");if(f||p){let m=l.getPropertyValue("font-family"),g=parseInt(l.getPropertyValue("font-size"))||32,y=parseInt(l.getPropertyValue("font-weight"))||!1,C=l.getPropertyValue("color")||"#000",w=document.createElement("span");w.dataset.snapdomPseudo=c;let A=O(l),F=M(A,"span",t);o.set(w,F);let j=m&&/font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(m),E=X(d);if(!a&&j&&E.length===1){let b=document.createElement("img");b.src=await G(E,m,y,g,C),b.style="display:block;width:100%;height:100%;object-fit:contain;",w.appendChild(b)}else if(E.startsWith("url(")){let b=$(E);if(b)try{let v=document.createElement("img"),ie=await S(encodeURI(b));v.src=ie,v.style="display:block;width:100%;height:100%;object-fit:contain;",w.appendChild(v)}catch(v){console.error(`[snapdom] Error in pseudo ${c} for`,e,v)}}else!j&&E&&E!=="none"&&(w.textContent=E);if(p&&h){let b=$(h);if(b)try{let v=await S(encodeURI(b));w.style.backgroundImage=`url(${v})`}catch(v){console.warn(`[snapdom] Failed to inline background-image for ${c}`,v)}}if(!(w.childNodes.length>0||w.textContent&&w.textContent.trim()!=="")&&!p)continue;c==="::before"?n.insertBefore(w,n.firstChild):n.appendChild(w)}}catch(l){console.warn(`[snapdom] Failed to capture ${c} for`,e,l)}let s=Array.from(e.children),i=Array.from(n.children).filter(c=>!c.dataset.snapdomPseudo);for(let c=0;c<Math.min(s.length,i.length);c++)await V(s[c],i[c],o,r,t,a)}async function Q(e,n=!1,o=!1){let r=new Map,t=new WeakMap,a=new Map,s;try{s=D(e,r,t,a,n)}catch(c){throw console.warn("deepClone failed:",c),c}try{await V(e,s,r,t,n,o)}catch(c){console.warn("inlinePseudoElements failed:",c)}let i="";if(n){let c=J(r);i=Array.from(c.entries()).map(([l,d])=>`.${d}{${l}}`).join("");for(let[l,d]of r.entries()){if(l.tagName==="STYLE")continue;let h=c.get(d);h&&l.classList.add(h);let f=l.style?.backgroundImage;l.removeAttribute("style"),f&&f!=="none"&&(l.style.backgroundImage=f)}}else for(let[c,l]of r.entries())c.tagName!=="STYLE"&&c.setAttribute("style",l.replace(/;/g,"; "));for(let[c,l]of a.entries()){let d=l.scrollLeft,h=l.scrollTop;if((d||h)&&c instanceof HTMLElement){c.style.overflow="hidden",c.style.scrollbarWidth="none",c.style.msOverflowStyle="none";let p=document.createElement("div");for(p.style.transform=`translate(${-d}px, ${-h}px)`,p.style.willChange="transform",p.style.display="inline-block",p.style.width="100%";c.firstChild;)p.appendChild(c.firstChild);c.appendChild(p)}}return{clone:s,classCSS:i,styleCache:t}}async function Z(e,n={}){let o=Array.from(e.querySelectorAll("img")),r=async t=>{let a=t.src;try{let s=n.crossOrigin?n.crossOrigin(a):"anonymous",i=await S(a,3e3,s);t.src=i,t.width||(t.width=t.naturalWidth||100),t.height||(t.height=t.naturalHeight||100)}catch{let s=document.createElement("div");s.style=`width: ${t.width||100}px; height: ${t.height||100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${t.height||100}px; color: #666; font-size: 12px;`,s.innerText="img",t.replaceWith(s)}};for(let t=0;t<o.length;t+=4){let a=o.slice(t,t+4).map(r);await Promise.allSettled(a)}}async function ee(e,n,o,r={}){let t=[[e,n]];for(;t.length;){let[a,s]=t.shift(),i=o.get(a)||U(a);o.has(a)||o.set(a,i);let l=i.getPropertyValue("background-image").split(",");for(let f=0;f<l.length;f++){let p=$(l[f]);if(p)try{let m=encodeURI(p),g;if(T.has(m))g=T.get(m);else{let y=r.crossOrigin?r.crossOrigin(m):"anonymous";g=await S(m,3e3,y),T.set(m,g)}l[f]=`url(${g})`}catch{l[f]="none"}}l.some(f=>f.includes("url("))&&(s.style.backgroundImage=l.join(","));let d=Array.from(a.children),h=Array.from(s.children);for(let f=0;f<Math.min(d.length,h.length);f++)t.push([d[f],h[f]])}}async function te(e,n={}){if(!e)throw new Error("Element cannot be null or undefined");let{compress:o=!0,embedFonts:r=!1,fast:t=!0,scale:a=1}=n,s,i,c,l="",d="",h,f;if({clone:s,classCSS:i,styleCache:c}=await Q(e,o,r),await new Promise(m=>{L(async()=>{await Z(s,n),m()},{fast:t})}),await new Promise(m=>{L(async()=>{await ee(e,s,c,n),m()},{fast:t})}),r&&await new Promise(m=>{L(async()=>{l=await W({ignoreIconFonts:!r}),m()},{fast:t})}),o){let m=Y(s).sort(),g=m.join(",");k.has(g)?d=k.get(g):await new Promise(y=>{L(()=>{d=K(m),k.set(g,d),y()},{fast:t})})}await new Promise(m=>{L(()=>{let g=e.getBoundingClientRect(),y=Math.ceil(g.width),C=Math.ceil(g.height);s.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),s.style.transformOrigin="top left",a!==1&&B()&&(s.style.scale=`${a}`);let w="http://www.w3.org/2000/svg",A=document.createElementNS(w,"foreignObject");A.setAttribute("width","100%"),A.setAttribute("height","100%");let F=document.createElement("style");F.textContent=d+l+"svg{overflow:visible;}"+i,A.appendChild(F),A.appendChild(s);let E=new XMLSerializer().serializeToString(A);f=`<svg xmlns="${w}" width="${y}" height="${C}" viewBox="0 0 ${y} ${C}">`+E+"</svg>",h=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(f)}`,m()},{fast:t})});let p=document.getElementById("snapdom-sandbox");return p&&p.style.position==="absolute"&&p.remove(),h}async function le(e,{dpr:n=1,scale:o=1}){let r=new Image;return r.src=e,await r.decode(),B?(r.width=r.width*o,r.height=r.height*o):(r.width=r.width/o,r.height=r.height/o),r}async function ne(e,{dpr:n=1,scale:o=1}={}){let r=new Image;r.src=e,await r.decode();let t=document.createElement("canvas"),a=r.width*o,s=r.height*o;t.width=Math.ceil(a*n),t.height=Math.ceil(s*n);let i=t.getContext("2d");return i.scale(n,n),i.drawImage(r,0,0,a,s),t.style.width=`${a}px`,t.style.height=`${s}px`,t}async function oe(e){let n=decodeURIComponent(e.split(",")[1]);return new Blob([n],{type:"image/svg+xml"})}async function ae(e,{dpr:n=1,scale:o=1},r){let t=await ne(e,{dpr:n,scale:o});if(!r)return t;let a=document.createElement("canvas");a.width=t.width,a.height=t.height;let s=a.getContext("2d");return s.fillStyle=r,s.fillRect(0,0,a.width,a.height),s.drawImage(t,0,0),a}async function q(e,{dpr:n=1,scale:o=1,backgroundColor:r="#fff",quality:t},a="png"){let s=await ae(e,{dpr:n,scale:o},r),i=new Image;return i.src=s.toDataURL(`image/${a}`,t),await i.decode(),i.style.width=`${s.width/n}px`,i.style.height=`${s.height/n}px`,i}async function fe(e,{dpr:n=1,scale:o=1,backgroundColor:r="#fff",format:t="png",filename:a="capture"}={}){if(t==="svg"){let f=await oe(e),p=URL.createObjectURL(f),m=document.createElement("a");m.href=p,m.download=`${a}.svg`,m.click(),URL.revokeObjectURL(p);return}let s=["jpg","jpeg","webp"].includes(t)?"#fff":void 0,c=await ae(e,{dpr:n,scale:o},r??s),l={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",webp:"image/webp"}[t]||"image/png",d=c.toDataURL(l),h=document.createElement("a");h.href=d,h.download=`${a}.${t}`,h.click()}async function u(e,n={}){if(n={scale:1,...n},!e)throw new Error("Element cannot be null or undefined");return await u.capture(e,n)}u.capture=async(e,n={})=>{let o=await te(e,n),r=window.devicePixelRatio||1,t=n.scale||1;return{url:o,options:n,toRaw:()=>o,toImg:()=>le(o,{dpr:r,scale:t}),toCanvas:()=>ne(o,{dpr:r,scale:t}),toBlob:()=>oe(o),toPng:a=>q(o,{dpr:r,scale:t,...a},"png"),toJpg:a=>q(o,{dpr:r,scale:t,...a},"jpeg"),toWebp:a=>q(o,{dpr:r,scale:t,...a},"webp"),download:({format:a="png",filename:s="capture",backgroundColor:i}={})=>fe(o,{dpr:r,scale:t,backgroundColor:i,format:a,filename:s})}};u.toRaw=async(e,n)=>(await u.capture(e,n)).toRaw();u.toImg=async(e,n)=>(await u.capture(e,n)).toImg();u.toCanvas=async(e,n)=>(await u.capture(e,n)).toCanvas();u.toBlob=async(e,n)=>(await u.capture(e,n)).toBlob();u.toPng=async(e,n)=>(await u.capture(e,n)).toPng(n);u.toJpg=async(e,n)=>(await u.capture(e,n)).toJpg(n);u.toWebp=async(e,n)=>(await u.capture(e,n)).toWebp(n);u.download=async(e,n={})=>{let{format:o="png",filename:r="capture",backgroundColor:t,...a}=n;return await(await u.capture(e,a)).download({format:o,filename:r,backgroundColor:t})};async function re(e=document,n={}){let{embedFonts:o=!0,reset:r=!1,crossOrigin:t}=n;if(r){R.clear(),T.clear(),x.clear(),k.clear(),N.clear();return}await document.fonts.ready,H();let a=[],s=[];e?.querySelectorAll&&(a=Array.from(e.querySelectorAll("img[src]")),s=Array.from(e.querySelectorAll("*")));let i=[];for(let c of a){let l=c.src;if(!R.has(l)){let d=t?t(l):"anonymous";i.push(S(l,3e3,d).then(h=>R.set(l,h)).catch(()=>{}))}}for(let c of s){let l=getComputedStyle(c).backgroundImage,d=$(l);if(d&&!T.has(d)){let h=t?t(d):"anonymous";i.push(S(d,3e3,h).then(f=>T.set(d,f)).catch(()=>{}))}}o&&await W({ignoreIconFonts:!o,preCached:!0}),await Promise.all(i)}window.snapdom=u;window.preCache=re;})();
|
package/dist/snapdom.min.mjs
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
var
|
|
2
|
-
`;return o}function J(
|
|
3
|
-
`}catch{console.warn("
|
|
1
|
+
var R=new Map,T=new Map,C=new Map,I=new Map,k=new Map,N=new WeakMap;var ie=["div","span","p","a","img","ul","li","button","input","select","textarea","label","section","article","header","footer","nav","main","aside","h1","h2","h3","h4","h5","h6","svg","path","circle","rect","line","g","table","thead","tbody","tr","td","th"];function H(){for(let e of ie)z(e)}function z(e){if(I.has(e))return I.get(e);if(new Set(["script","style","meta","link","noscript","template"]).has(e)){let s={};return I.set(e,s),s}let o=document.getElementById("snapdom-sandbox");o||(o=document.createElement("div"),o.id="snapdom-sandbox",o.style.position="absolute",o.style.left="-9999px",o.style.top="-9999px",o.style.width="0",o.style.height="0",o.style.overflow="hidden",document.body.appendChild(o));let r=document.createElement(e);r.style.all="initial",o.appendChild(r);let t=getComputedStyle(r),a={};for(let s of t)a[s]=t.getPropertyValue(s);return o.removeChild(r),I.set(e,a),a}function M(e,n,o=!1){let r=[],t=z(n);for(let[a,s]of Object.entries(e))if(!o)s&&r.push(`${a}:${s}`);else{let i=t[a];s&&s!==i&&r.push(`${a}:${s}`)}return r.sort().join(";")}function Y(e){let n=new Set;return e.nodeType!==Node.ELEMENT_NODE&&e.nodeType!==Node.DOCUMENT_FRAGMENT_NODE?[]:(e.tagName&&n.add(e.tagName.toLowerCase()),typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(o=>n.add(o.tagName.toLowerCase())),Array.from(n))}function K(e){let n=new Map;for(let r of e){let t=I.get(r);if(!t)continue;let a=Object.entries(t).map(([s,i])=>`${s}:${i};`).sort().join("");n.has(a)||n.set(a,[]),n.get(a).push(r)}let o="";for(let[r,t]of n.entries())o+=`${t.join(",")} { ${r} }
|
|
2
|
+
`;return o}function J(e){let n=new Set(e.values()),o=new Map,r=1;for(let t of n)o.set(t,`c${r++}`);return o}function L(e,{fast:n=!1}={}){if(n)return e();"requestIdleCallback"in window?requestIdleCallback(e,{timeout:50}):setTimeout(e,1)}function U(e,n=null){if(!(e instanceof Element))return window.getComputedStyle(e,n);let o=N.get(e);if(o||(o=new Map,N.set(e,o)),!o.has(n)){let r=window.getComputedStyle(e,n);o.set(n,r)}return o.get(n)}function X(e){let n=e.replace(/^['"]|['"]$/g,"");if(n.startsWith("\\"))try{return String.fromCharCode(parseInt(n.replace("\\",""),16))}catch{return n}return n}function $(e){let n=e.indexOf("url(");if(n===-1)return null;let o=e.slice(n+4).trim();return o.endsWith(")")&&(o=o.slice(0,-1).trim()),(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),o}function P(e){return[/font\s*awesome/i,/material\s*icons/i,/ionicons/i,/glyphicons/i,/feather/i,/bootstrap\s*icons/i,/remix\s*icons/i,/heroicons/i].some(o=>o.test(e))}function S(e,n=3e3,o="anonymous"){return R.has(e)?Promise.resolve(R.get(e)):new Promise((r,t)=>{let a=setTimeout(()=>{t(new Error("Image load timed out"))},n),s=new Image;s.crossOrigin=o,s.onload=async()=>{clearTimeout(a);try{await s.decode();let i=document.createElement("canvas");i.width=s.width,i.height=s.height,i.getContext("2d").drawImage(s,0,0,i.width,i.height);try{let l=i.toDataURL("image/png");R.set(e,l),r(l)}catch{t(new Error("CORS restrictions prevented image capture"))}}catch(i){t(i)}},s.onerror=i=>{clearTimeout(a),t(new Error("Failed to load image: "+(i.message||"Unknown error")))},s.src=e})}function O(e){let n={};for(let o of e)n[o]=e.getPropertyValue(o);return n}function B(){return/^((?!chrome|android).)*safari/i.test(navigator.userAgent)}function _(e,n,o,r,t){if(e.tagName==="STYLE")return;r.has(e)||r.set(e,U(e));let a=r.get(e),s=O(a),i=e.tagName?.toLowerCase()||"div",c=M(s,i,t);o.set(n,c)}function D(e,n,o,r,t){if(e.nodeType===Node.ELEMENT_NODE&&e.getAttribute("data-capture")==="exclude"){let i=document.createElement("div"),c=e.getBoundingClientRect();return i.style.cssText=`display: inline-block; width: ${c.width}px; height: ${c.height}px; visibility: hidden;`,i}if(e.tagName==="IFRAME"){let i=document.createElement("div");return i.textContent="",i.style.cssText=`width: ${e.offsetWidth}px; height: ${e.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;`,i}if(e.nodeType===Node.ELEMENT_NODE&&e.getAttribute("data-capture")==="placeholder"){let i=e.cloneNode(!1);r.set(i,e),_(e,i,n,o,t);let c=document.createElement("div");return c.textContent=e.getAttribute("data-placeholder-text")||"",c.style.cssText="color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;",i.appendChild(c),i}if(e.tagName==="CANVAS"){let i=e.toDataURL(),c=document.createElement("img");return c.src=i,c.width=e.width,c.height=e.height,c.style.display="inline-block",c.style.width=`${e.width}px`,c.style.height=`${e.height}px`,c}if(e.nodeType===Node.TEXT_NODE){if(e.parentElement?.shadowRoot){let i=e.parentElement.tagName.toLowerCase();if(customElements.get(i))return null}return e.cloneNode(!0)}if(e.nodeType!==Node.ELEMENT_NODE)return e.cloneNode(!0);let a=e.cloneNode(!1);r.set(a,e),e instanceof HTMLInputElement?(a.value=e.value,a.setAttribute("value",e.value),e.checked!==void 0&&(a.checked=e.checked,e.checked&&a.setAttribute("checked",""))):e instanceof HTMLTextAreaElement?(a.value=e.value,a.textContent=e.value):e instanceof HTMLSelectElement&&(a.value=e.value,Array.from(a.options).forEach(i=>{i.value===e.value?i.setAttribute("selected",""):i.removeAttribute("selected")})),_(e,a,n,o,t);let s=document.createDocumentFragment();if(e.childNodes.forEach(i=>{let c=D(i,n,o,r,t);c&&s.appendChild(c)}),a.appendChild(s),e.shadowRoot){let i=Array.from(e.shadowRoot.children).filter(l=>l.tagName!=="STYLE").map(l=>D(l,n,o,r)).filter(Boolean),c=document.createDocumentFragment();i.forEach(l=>c.appendChild(l)),a.appendChild(c)}return a}async function G(e,n,o,r=32,t="#000"){n=n.replace(/^['"]+|['"]+$/g,"");let a=window.devicePixelRatio||1,i=document.createElement("canvas").getContext("2d");i.font=o?`${o} ${r}px "${n}"`:`${r}px "${n}"`;let c=i.measureText(e),l=c.actualBoundingBoxAscent||r*.8,d=c.actualBoundingBoxDescent||r*.2,h=l+d,f=c.width,p=document.createElement("canvas");p.width=Math.ceil(f*a),p.height=Math.ceil(h*a);let m=p.getContext("2d");return m.scale(a,a),m.font=i.font,m.textAlign="left",m.textBaseline="alphabetic",m.fillStyle=t,m.fillText(e,0,l),p.toDataURL()}async function W({ignoreIconFonts:e=!0,preCached:n=!1}={}){let o=Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(t=>t.href),r="";for(let t of o)try{let s=await(await fetch(t.href)).text();if(e&&(P(t.href)||P(s)))continue;let i=/url\(([^)]+)\)/g,c=await Promise.all(Array.from(s.matchAll(i)).map(async d=>{let h=$(d[0]);if(!h)return null;let f=h;f.startsWith("http")||(f=new URL(f,t.href).href);try{f=encodeURI(f)}catch{console.warn("[snapdom] Failed to encode font URL:",f)}if(e&&P(f))return null;if(C.has(f))return{original:d[0],inlined:`url(${C.get(f)})`};try{let m=await(await fetch(f)).blob(),u=await new Promise(y=>{let x=new FileReader;x.onload=()=>y(x.result),x.readAsDataURL(m)});return C.set(f,u),{original:d[0],inlined:`url(${u})`}}catch{return console.warn("[snapdom] Failed to fetch font:",f),null}})),l=s;for(let d of c)d&&(l=l.replace(d.original,d.inlined));r+=l+`
|
|
3
|
+
`}catch{console.warn("[snapdom] Failed to fetch CSS:",t.href)}for(let t of document.styleSheets)try{if(!t.href||o.every(a=>a.href!==t.href)){for(let a of t.cssRules)if(a.type===CSSRule.FONT_FACE_RULE){let s=a.style.getPropertyValue("src");if(!s)continue;let i=/url\(([^)]+)\)/g,c=s,l=Array.from(s.matchAll(i));for(let d of l){let h=d[1].trim().replace(/^["']|["']$/g,"");if(!h)continue;let f=h;if(!f.startsWith("http")&&!f.startsWith("data:")&&(f=new URL(f,t.href||location.href).href),!(e&&P(f))){if(C.has(f)){c=c.replace(d[0],`url(${C.get(f)})`);continue}try{let m=await(await fetch(f)).blob(),u=await new Promise(y=>{let x=new FileReader;x.onload=()=>y(x.result),x.readAsDataURL(m)});C.set(f,u),c=c.replace(d[0],`url(${u})`)}catch{console.warn("[snapdom] Failed to fetch font URL:",f)}}}r+=`@font-face {
|
|
4
|
+
font-family: ${a.style.getPropertyValue("font-family")};
|
|
5
|
+
src: ${c};
|
|
6
|
+
font-style: ${a.style.getPropertyValue("font-style")||"normal"};
|
|
7
|
+
font-weight: ${a.style.getPropertyValue("font-weight")||"normal"};
|
|
8
|
+
}
|
|
9
|
+
`}}}catch(a){console.warn("[snapdom] Cannot access stylesheet",t.href,a)}for(let t of document.fonts)if(t.family&&t.status==="loaded"&&t._snapdomSrc){let a=t._snapdomSrc;if(!a.startsWith("data:"))if(C.has(t._snapdomSrc))a=C.get(t._snapdomSrc);else try{let i=await(await fetch(t._snapdomSrc)).blob();a=await new Promise(c=>{let l=new FileReader;l.onload=()=>c(l.result),l.readAsDataURL(i)}),C.set(t._snapdomSrc,a)}catch{console.warn("[snapdom] Failed to fetch dynamic font src:",t._snapdomSrc);continue}r+=`@font-face {
|
|
10
|
+
font-family: '${t.family}';
|
|
11
|
+
src: url(${a});
|
|
12
|
+
font-style: ${t.style||"normal"};
|
|
13
|
+
font-weight: ${t.weight||"normal"};
|
|
14
|
+
}
|
|
15
|
+
`}if(r&&n){let t=document.createElement("style");t.setAttribute("data-snapdom","embedFonts"),t.textContent=r,document.head.appendChild(t)}return r}async function V(e,n,o,r,t,a=!1){if(!(e instanceof Element)||!(n instanceof Element))return;for(let c of["::before","::after"])try{let l=U(e,c);if(!l)continue;let d=l.getPropertyValue("content"),h=l.getPropertyValue("background-image"),f=d&&d!=="none"&&d!=='""'&&d!=="''",p=h&&h.startsWith("url(");if(f||p){let m=l.getPropertyValue("font-family"),u=parseInt(l.getPropertyValue("font-size"))||32,y=parseInt(l.getPropertyValue("font-weight"))||!1,x=l.getPropertyValue("color")||"#000",w=document.createElement("span");w.dataset.snapdomPseudo=c;let A=O(l),F=M(A,"span",t);o.set(w,F);let j=m&&/font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(m),E=X(d);if(!a&&j&&E.length===1){let b=document.createElement("img");b.src=await G(E,m,y,u,x),b.style="display:block;width:100%;height:100%;object-fit:contain;",w.appendChild(b)}else if(E.startsWith("url(")){let b=$(E);if(b)try{let v=document.createElement("img"),se=await S(encodeURI(b));v.src=se,v.style="display:block;width:100%;height:100%;object-fit:contain;",w.appendChild(v)}catch(v){console.error(`[snapdom] Error in pseudo ${c} for`,e,v)}}else!j&&E&&E!=="none"&&(w.textContent=E);if(p&&h){let b=$(h);if(b)try{let v=await S(encodeURI(b));w.style.backgroundImage=`url(${v})`}catch(v){console.warn(`[snapdom] Failed to inline background-image for ${c}`,v)}}if(!(w.childNodes.length>0||w.textContent&&w.textContent.trim()!=="")&&!p)continue;c==="::before"?n.insertBefore(w,n.firstChild):n.appendChild(w)}}catch(l){console.warn(`[snapdom] Failed to capture ${c} for`,e,l)}let s=Array.from(e.children),i=Array.from(n.children).filter(c=>!c.dataset.snapdomPseudo);for(let c=0;c<Math.min(s.length,i.length);c++)await V(s[c],i[c],o,r,t,a)}async function Q(e,n=!1,o=!1){let r=new Map,t=new WeakMap,a=new Map,s;try{s=D(e,r,t,a,n)}catch(c){throw console.warn("deepClone failed:",c),c}try{await V(e,s,r,t,n,o)}catch(c){console.warn("inlinePseudoElements failed:",c)}let i="";if(n){let c=J(r);i=Array.from(c.entries()).map(([l,d])=>`.${d}{${l}}`).join("");for(let[l,d]of r.entries()){if(l.tagName==="STYLE")continue;let h=c.get(d);h&&l.classList.add(h);let f=l.style?.backgroundImage;l.removeAttribute("style"),f&&f!=="none"&&(l.style.backgroundImage=f)}}else for(let[c,l]of r.entries())c.tagName!=="STYLE"&&c.setAttribute("style",l.replace(/;/g,"; "));for(let[c,l]of a.entries()){let d=l.scrollLeft,h=l.scrollTop;if((d||h)&&c instanceof HTMLElement){c.style.overflow="hidden",c.style.scrollbarWidth="none",c.style.msOverflowStyle="none";let p=document.createElement("div");for(p.style.transform=`translate(${-d}px, ${-h}px)`,p.style.willChange="transform",p.style.display="inline-block",p.style.width="100%";c.firstChild;)p.appendChild(c.firstChild);c.appendChild(p)}}return{clone:s,classCSS:i,styleCache:t}}async function Z(e,n={}){let o=Array.from(e.querySelectorAll("img")),r=async t=>{let a=t.src;try{let s=n.crossOrigin?n.crossOrigin(a):"anonymous",i=await S(a,3e3,s);t.src=i,t.width||(t.width=t.naturalWidth||100),t.height||(t.height=t.naturalHeight||100)}catch{let s=document.createElement("div");s.style=`width: ${t.width||100}px; height: ${t.height||100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${t.height||100}px; color: #666; font-size: 12px;`,s.innerText="img",t.replaceWith(s)}};for(let t=0;t<o.length;t+=4){let a=o.slice(t,t+4).map(r);await Promise.allSettled(a)}}async function ee(e,n,o,r={}){let t=[[e,n]];for(;t.length;){let[a,s]=t.shift(),i=o.get(a)||U(a);o.has(a)||o.set(a,i);let l=i.getPropertyValue("background-image").split(",");for(let f=0;f<l.length;f++){let p=$(l[f]);if(p)try{let m=encodeURI(p),u;if(T.has(m))u=T.get(m);else{let y=r.crossOrigin?r.crossOrigin(m):"anonymous";u=await S(m,3e3,y),T.set(m,u)}l[f]=`url(${u})`}catch{l[f]="none"}}l.some(f=>f.includes("url("))&&(s.style.backgroundImage=l.join(","));let d=Array.from(a.children),h=Array.from(s.children);for(let f=0;f<Math.min(d.length,h.length);f++)t.push([d[f],h[f]])}}async function te(e,n={}){if(!e)throw new Error("Element cannot be null or undefined");let{compress:o=!0,embedFonts:r=!1,fast:t=!0,scale:a=1}=n,s,i,c,l="",d="",h,f;if({clone:s,classCSS:i,styleCache:c}=await Q(e,o,r),await new Promise(m=>{L(async()=>{await Z(s,n),m()},{fast:t})}),await new Promise(m=>{L(async()=>{await ee(e,s,c,n),m()},{fast:t})}),r&&await new Promise(m=>{L(async()=>{l=await W({ignoreIconFonts:!r}),m()},{fast:t})}),o){let m=Y(s).sort(),u=m.join(",");k.has(u)?d=k.get(u):await new Promise(y=>{L(()=>{d=K(m),k.set(u,d),y()},{fast:t})})}await new Promise(m=>{L(()=>{let u=e.getBoundingClientRect(),y=Math.ceil(u.width),x=Math.ceil(u.height);s.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),s.style.transformOrigin="top left",a!==1&&B()&&(s.style.scale=`${a}`);let w="http://www.w3.org/2000/svg",A=document.createElementNS(w,"foreignObject");A.setAttribute("width","100%"),A.setAttribute("height","100%");let F=document.createElement("style");F.textContent=d+l+"svg{overflow:visible;}"+i,A.appendChild(F),A.appendChild(s);let E=new XMLSerializer().serializeToString(A);f=`<svg xmlns="${w}" width="${y}" height="${x}" viewBox="0 0 ${y} ${x}">`+E+"</svg>",h=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(f)}`,m()},{fast:t})});let p=document.getElementById("snapdom-sandbox");return p&&p.style.position==="absolute"&&p.remove(),h}async function ce(e,{dpr:n=1,scale:o=1}){let r=new Image;return r.src=e,await r.decode(),B?(r.width=r.width*o,r.height=r.height*o):(r.width=r.width/o,r.height=r.height/o),r}async function ne(e,{dpr:n=1,scale:o=1}={}){let r=new Image;r.src=e,await r.decode();let t=document.createElement("canvas"),a=r.width*o,s=r.height*o;t.width=Math.ceil(a*n),t.height=Math.ceil(s*n);let i=t.getContext("2d");return i.scale(n,n),i.drawImage(r,0,0,a,s),t.style.width=`${a}px`,t.style.height=`${s}px`,t}async function oe(e){let n=decodeURIComponent(e.split(",")[1]);return new Blob([n],{type:"image/svg+xml"})}async function ae(e,{dpr:n=1,scale:o=1},r){let t=await ne(e,{dpr:n,scale:o});if(!r)return t;let a=document.createElement("canvas");a.width=t.width,a.height=t.height;let s=a.getContext("2d");return s.fillStyle=r,s.fillRect(0,0,a.width,a.height),s.drawImage(t,0,0),a}async function q(e,{dpr:n=1,scale:o=1,backgroundColor:r="#fff",quality:t},a="png"){let s=await ae(e,{dpr:n,scale:o},r),i=new Image;return i.src=s.toDataURL(`image/${a}`,t),await i.decode(),i.style.width=`${s.width/n}px`,i.style.height=`${s.height/n}px`,i}async function le(e,{dpr:n=1,scale:o=1,backgroundColor:r="#fff",format:t="png",filename:a="capture"}={}){if(t==="svg"){let f=await oe(e),p=URL.createObjectURL(f),m=document.createElement("a");m.href=p,m.download=`${a}.svg`,m.click(),URL.revokeObjectURL(p);return}let s=["jpg","jpeg","webp"].includes(t)?"#fff":void 0,c=await ae(e,{dpr:n,scale:o},r??s),l={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",webp:"image/webp"}[t]||"image/png",d=c.toDataURL(l),h=document.createElement("a");h.href=d,h.download=`${a}.${t}`,h.click()}async function g(e,n={}){if(n={scale:1,...n},!e)throw new Error("Element cannot be null or undefined");return await g.capture(e,n)}g.capture=async(e,n={})=>{let o=await te(e,n),r=window.devicePixelRatio||1,t=n.scale||1;return{url:o,options:n,toRaw:()=>o,toImg:()=>ce(o,{dpr:r,scale:t}),toCanvas:()=>ne(o,{dpr:r,scale:t}),toBlob:()=>oe(o),toPng:a=>q(o,{dpr:r,scale:t,...a},"png"),toJpg:a=>q(o,{dpr:r,scale:t,...a},"jpeg"),toWebp:a=>q(o,{dpr:r,scale:t,...a},"webp"),download:({format:a="png",filename:s="capture",backgroundColor:i}={})=>le(o,{dpr:r,scale:t,backgroundColor:i,format:a,filename:s})}};g.toRaw=async(e,n)=>(await g.capture(e,n)).toRaw();g.toImg=async(e,n)=>(await g.capture(e,n)).toImg();g.toCanvas=async(e,n)=>(await g.capture(e,n)).toCanvas();g.toBlob=async(e,n)=>(await g.capture(e,n)).toBlob();g.toPng=async(e,n)=>(await g.capture(e,n)).toPng(n);g.toJpg=async(e,n)=>(await g.capture(e,n)).toJpg(n);g.toWebp=async(e,n)=>(await g.capture(e,n)).toWebp(n);g.download=async(e,n={})=>{let{format:o="png",filename:r="capture",backgroundColor:t,...a}=n;return await(await g.capture(e,a)).download({format:o,filename:r,backgroundColor:t})};async function fe(e=document,n={}){let{embedFonts:o=!0,reset:r=!1,crossOrigin:t}=n;if(r){R.clear(),T.clear(),C.clear(),k.clear(),N.clear();return}await document.fonts.ready,H();let a=[],s=[];e?.querySelectorAll&&(a=Array.from(e.querySelectorAll("img[src]")),s=Array.from(e.querySelectorAll("*")));let i=[];for(let c of a){let l=c.src;if(!R.has(l)){let d=t?t(l):"anonymous";i.push(S(l,3e3,d).then(h=>R.set(l,h)).catch(()=>{}))}}for(let c of s){let l=getComputedStyle(c).backgroundImage,d=$(l);if(d&&!T.has(d)){let h=t?t(d):"anonymous";i.push(S(d,3e3,h).then(f=>T.set(d,f)).catch(()=>{}))}}o&&await W({ignoreIconFonts:!o,preCached:!0}),await Promise.all(i)}export{fe as preCache,g as snapdom};
|
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.
|
|
4
|
+
* v.1.7.1
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -209,7 +209,7 @@ function isIconFont(familyOrUrl) {
|
|
|
209
209
|
];
|
|
210
210
|
return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
|
|
211
211
|
}
|
|
212
|
-
function fetchImage(src, timeout = 3e3) {
|
|
212
|
+
function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
213
213
|
if (imageCache.has(src)) {
|
|
214
214
|
return Promise.resolve(imageCache.get(src));
|
|
215
215
|
}
|
|
@@ -218,7 +218,7 @@ function fetchImage(src, timeout = 3e3) {
|
|
|
218
218
|
reject(new Error("Image load timed out"));
|
|
219
219
|
}, timeout);
|
|
220
220
|
const image = new Image();
|
|
221
|
-
image.crossOrigin =
|
|
221
|
+
image.crossOrigin = crossOrigin;
|
|
222
222
|
image.onload = async () => {
|
|
223
223
|
clearTimeout(timeoutId);
|
|
224
224
|
try {
|
|
@@ -227,7 +227,7 @@ function fetchImage(src, timeout = 3e3) {
|
|
|
227
227
|
canvas.width = image.width;
|
|
228
228
|
canvas.height = image.height;
|
|
229
229
|
const ctx = canvas.getContext("2d");
|
|
230
|
-
ctx.drawImage(image, 0, 0);
|
|
230
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
231
231
|
try {
|
|
232
232
|
const dataURL = canvas.toDataURL("image/png");
|
|
233
233
|
imageCache.set(src, dataURL);
|
|
@@ -315,6 +315,26 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
315
315
|
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
316
316
|
const clone = node.cloneNode(false);
|
|
317
317
|
nodeMap.set(clone, node);
|
|
318
|
+
if (node instanceof HTMLInputElement) {
|
|
319
|
+
clone.value = node.value;
|
|
320
|
+
clone.setAttribute("value", node.value);
|
|
321
|
+
if (node.checked !== void 0) {
|
|
322
|
+
clone.checked = node.checked;
|
|
323
|
+
if (node.checked) clone.setAttribute("checked", "");
|
|
324
|
+
}
|
|
325
|
+
} else if (node instanceof HTMLTextAreaElement) {
|
|
326
|
+
clone.value = node.value;
|
|
327
|
+
clone.textContent = node.value;
|
|
328
|
+
} else if (node instanceof HTMLSelectElement) {
|
|
329
|
+
clone.value = node.value;
|
|
330
|
+
Array.from(clone.options).forEach((opt) => {
|
|
331
|
+
if (opt.value === node.value) {
|
|
332
|
+
opt.setAttribute("selected", "");
|
|
333
|
+
} else {
|
|
334
|
+
opt.removeAttribute("selected");
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
318
338
|
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
319
339
|
const frag = document.createDocumentFragment();
|
|
320
340
|
node.childNodes.forEach((child) => {
|
|
@@ -355,7 +375,7 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
355
375
|
ctx.fillText(unicodeChar, 0, ascent);
|
|
356
376
|
return canvas.toDataURL();
|
|
357
377
|
}
|
|
358
|
-
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
378
|
+
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
|
|
359
379
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
|
|
360
380
|
let finalCSS = "";
|
|
361
381
|
for (const link of links) {
|
|
@@ -396,7 +416,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
|
396
416
|
resourceCache.set(url, b64);
|
|
397
417
|
return { original: match[0], inlined: `url(${b64})` };
|
|
398
418
|
} catch (err) {
|
|
399
|
-
console.warn("
|
|
419
|
+
console.warn("[snapdom] Failed to fetch font:", url);
|
|
400
420
|
return null;
|
|
401
421
|
}
|
|
402
422
|
})
|
|
@@ -409,7 +429,90 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
|
409
429
|
}
|
|
410
430
|
finalCSS += cssFinal + "\n";
|
|
411
431
|
} catch (e) {
|
|
412
|
-
console.warn("
|
|
432
|
+
console.warn("[snapdom] Failed to fetch CSS:", link.href);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const sheet of document.styleSheets) {
|
|
436
|
+
try {
|
|
437
|
+
if (!sheet.href || links.every((link) => link.href !== sheet.href)) {
|
|
438
|
+
for (const rule of sheet.cssRules) {
|
|
439
|
+
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
440
|
+
const src = rule.style.getPropertyValue("src");
|
|
441
|
+
if (!src) continue;
|
|
442
|
+
const urlRegex = /url\(([^)]+)\)/g;
|
|
443
|
+
let inlinedSrc = src;
|
|
444
|
+
const matches = Array.from(src.matchAll(urlRegex));
|
|
445
|
+
for (const match of matches) {
|
|
446
|
+
let rawUrl = match[1].trim().replace(/^["']|["']$/g, "");
|
|
447
|
+
if (!rawUrl) continue;
|
|
448
|
+
let url = rawUrl;
|
|
449
|
+
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
450
|
+
url = new URL(url, sheet.href || location.href).href;
|
|
451
|
+
}
|
|
452
|
+
if (ignoreIconFonts && isIconFont(url)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (resourceCache.has(url)) {
|
|
456
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const res = await fetch(url);
|
|
461
|
+
const blob = await res.blob();
|
|
462
|
+
const b64 = await new Promise((resolve) => {
|
|
463
|
+
const reader = new FileReader();
|
|
464
|
+
reader.onload = () => resolve(reader.result);
|
|
465
|
+
reader.readAsDataURL(blob);
|
|
466
|
+
});
|
|
467
|
+
resourceCache.set(url, b64);
|
|
468
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finalCSS += `@font-face {
|
|
474
|
+
font-family: ${rule.style.getPropertyValue("font-family")};
|
|
475
|
+
src: ${inlinedSrc};
|
|
476
|
+
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
477
|
+
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
478
|
+
}
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.warn("[snapdom] Cannot access stylesheet", sheet.href, e);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const font of document.fonts) {
|
|
488
|
+
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
489
|
+
let b64 = font._snapdomSrc;
|
|
490
|
+
if (!b64.startsWith("data:")) {
|
|
491
|
+
if (resourceCache.has(font._snapdomSrc)) {
|
|
492
|
+
b64 = resourceCache.get(font._snapdomSrc);
|
|
493
|
+
} else {
|
|
494
|
+
try {
|
|
495
|
+
const res = await fetch(font._snapdomSrc);
|
|
496
|
+
const blob = await res.blob();
|
|
497
|
+
b64 = await new Promise((resolve) => {
|
|
498
|
+
const reader = new FileReader();
|
|
499
|
+
reader.onload = () => resolve(reader.result);
|
|
500
|
+
reader.readAsDataURL(blob);
|
|
501
|
+
});
|
|
502
|
+
resourceCache.set(font._snapdomSrc, b64);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
finalCSS += `@font-face {
|
|
510
|
+
font-family: '${font.family}';
|
|
511
|
+
src: url(${b64});
|
|
512
|
+
font-style: ${font.style || "normal"};
|
|
513
|
+
font-weight: ${font.weight || "normal"};
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
413
516
|
}
|
|
414
517
|
}
|
|
415
518
|
if (finalCSS && preCached) {
|
|
@@ -467,18 +570,25 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
467
570
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
468
571
|
pseudoEl.appendChild(imgEl);
|
|
469
572
|
} catch (e) {
|
|
470
|
-
console.error(
|
|
471
|
-
`[snapdom] Error in pseudo ${pseudo} for`,
|
|
472
|
-
source,
|
|
473
|
-
e
|
|
474
|
-
);
|
|
573
|
+
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
475
574
|
}
|
|
476
575
|
}
|
|
477
576
|
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
478
577
|
pseudoEl.textContent = cleanContent;
|
|
479
578
|
}
|
|
579
|
+
if (hasBg && bg) {
|
|
580
|
+
const rawUrl = extractURL(bg);
|
|
581
|
+
if (rawUrl) {
|
|
582
|
+
try {
|
|
583
|
+
const dataUrl = await fetchImage(encodeURI(rawUrl));
|
|
584
|
+
pseudoEl.style.backgroundImage = `url(${dataUrl})`;
|
|
585
|
+
} catch (e) {
|
|
586
|
+
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
480
590
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
|
|
481
|
-
if (!hasContent2) continue;
|
|
591
|
+
if (!hasContent2 && !hasBg) continue;
|
|
482
592
|
if (pseudo === "::before") {
|
|
483
593
|
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
484
594
|
} else {
|
|
@@ -490,9 +600,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
490
600
|
}
|
|
491
601
|
}
|
|
492
602
|
const sChildren = Array.from(source.children);
|
|
493
|
-
const cChildren = Array.from(clone.children).filter(
|
|
494
|
-
(child) => !child.dataset.snapdomPseudo
|
|
495
|
-
);
|
|
603
|
+
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
496
604
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
497
605
|
await inlinePseudoElements(
|
|
498
606
|
sChildren[i],
|
|
@@ -563,12 +671,13 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
563
671
|
}
|
|
564
672
|
|
|
565
673
|
// src/modules/images.js
|
|
566
|
-
async function inlineImages(clone) {
|
|
674
|
+
async function inlineImages(clone, options = {}) {
|
|
567
675
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
568
676
|
const processImg = async (img) => {
|
|
569
677
|
const src = img.src;
|
|
570
678
|
try {
|
|
571
|
-
const
|
|
679
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
|
|
680
|
+
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
572
681
|
img.src = dataUrl;
|
|
573
682
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
574
683
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -586,29 +695,36 @@ async function inlineImages(clone) {
|
|
|
586
695
|
}
|
|
587
696
|
|
|
588
697
|
// src/modules/background.js
|
|
589
|
-
async function inlineBackgroundImages(source, clone, styleCache) {
|
|
698
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
590
699
|
const queue = [[source, clone]];
|
|
591
700
|
while (queue.length) {
|
|
592
701
|
const [srcNode, cloneNode] = queue.shift();
|
|
593
702
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
594
703
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
595
704
|
const bg = style.getPropertyValue("background-image");
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
dataUrl
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
705
|
+
const bgSplits = bg.split(",");
|
|
706
|
+
for (let i = 0; i < bgSplits.length; i++) {
|
|
707
|
+
const rawUrl = extractURL(bgSplits[i]);
|
|
708
|
+
if (rawUrl) {
|
|
709
|
+
try {
|
|
710
|
+
let bgUrl = encodeURI(rawUrl);
|
|
711
|
+
let dataUrl;
|
|
712
|
+
if (bgCache.has(bgUrl)) {
|
|
713
|
+
dataUrl = bgCache.get(bgUrl);
|
|
714
|
+
} else {
|
|
715
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(bgUrl) : "anonymous";
|
|
716
|
+
dataUrl = await fetchImage(bgUrl, 3e3, crossOrigin);
|
|
717
|
+
bgCache.set(bgUrl, dataUrl);
|
|
718
|
+
}
|
|
719
|
+
bgSplits[i] = `url(${dataUrl})`;
|
|
720
|
+
} catch {
|
|
721
|
+
bgSplits[i] = "none";
|
|
606
722
|
}
|
|
607
|
-
cloneNode.style.backgroundImage = `url(${dataUrl})`;
|
|
608
|
-
} catch {
|
|
609
|
-
cloneNode.style.backgroundImage = "none";
|
|
610
723
|
}
|
|
611
724
|
}
|
|
725
|
+
if (bgSplits.some((s) => s.includes("url("))) {
|
|
726
|
+
cloneNode.style.backgroundImage = bgSplits.join(",");
|
|
727
|
+
}
|
|
612
728
|
const sChildren = Array.from(srcNode.children);
|
|
613
729
|
const cChildren = Array.from(cloneNode.children);
|
|
614
730
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
@@ -629,13 +745,13 @@ async function captureDOM(element, options = {}) {
|
|
|
629
745
|
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
630
746
|
await new Promise((resolve) => {
|
|
631
747
|
idle(async () => {
|
|
632
|
-
await inlineImages(clone);
|
|
748
|
+
await inlineImages(clone, options);
|
|
633
749
|
resolve();
|
|
634
750
|
}, { fast });
|
|
635
751
|
});
|
|
636
752
|
await new Promise((resolve) => {
|
|
637
753
|
idle(async () => {
|
|
638
|
-
await inlineBackgroundImages(element, clone, styleCache);
|
|
754
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
639
755
|
resolve();
|
|
640
756
|
}, { fast });
|
|
641
757
|
});
|
|
@@ -708,20 +824,20 @@ async function toImg(url, { dpr = 1, scale = 1 }) {
|
|
|
708
824
|
}
|
|
709
825
|
return img;
|
|
710
826
|
}
|
|
711
|
-
async function toCanvas(url, { dpr = 1, scale = 1 }) {
|
|
827
|
+
async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
712
828
|
const img = new Image();
|
|
713
829
|
img.src = url;
|
|
714
830
|
await img.decode();
|
|
715
831
|
const canvas = document.createElement("canvas");
|
|
716
|
-
const width = img.width * scale
|
|
717
|
-
const height = img.height * scale
|
|
718
|
-
canvas.width = Math.ceil(width);
|
|
719
|
-
canvas.height = Math.ceil(height);
|
|
832
|
+
const width = img.width * scale;
|
|
833
|
+
const height = img.height * scale;
|
|
834
|
+
canvas.width = Math.ceil(width * dpr);
|
|
835
|
+
canvas.height = Math.ceil(height * dpr);
|
|
720
836
|
const ctx = canvas.getContext("2d");
|
|
721
|
-
ctx.scale(
|
|
722
|
-
ctx.drawImage(img, 0, 0);
|
|
723
|
-
canvas.style.width = `${
|
|
724
|
-
canvas.style.height = `${
|
|
837
|
+
ctx.scale(dpr, dpr);
|
|
838
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
839
|
+
canvas.style.width = `${width}px`;
|
|
840
|
+
canvas.style.height = `${height}px`;
|
|
725
841
|
return canvas;
|
|
726
842
|
}
|
|
727
843
|
async function toBlob(url) {
|
|
@@ -817,7 +933,7 @@ snapdom.download = async (el, options = {}) => {
|
|
|
817
933
|
|
|
818
934
|
// src/api/preCache.js
|
|
819
935
|
async function preCache(root = document, options = {}) {
|
|
820
|
-
const { embedFonts = true, reset = false,
|
|
936
|
+
const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
|
|
821
937
|
if (reset) {
|
|
822
938
|
imageCache.clear();
|
|
823
939
|
bgCache.clear();
|
|
@@ -837,8 +953,9 @@ async function preCache(root = document, options = {}) {
|
|
|
837
953
|
for (const img of imgEls) {
|
|
838
954
|
const src = img.src;
|
|
839
955
|
if (!imageCache.has(src)) {
|
|
956
|
+
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
840
957
|
promises.push(
|
|
841
|
-
fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
958
|
+
fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
842
959
|
})
|
|
843
960
|
);
|
|
844
961
|
}
|
|
@@ -847,8 +964,9 @@ async function preCache(root = document, options = {}) {
|
|
|
847
964
|
const bg = getComputedStyle(el).backgroundImage;
|
|
848
965
|
const url = extractURL(bg);
|
|
849
966
|
if (url && !bgCache.has(url)) {
|
|
967
|
+
const crossOrigin = crossOriginFn ? crossOriginFn(url) : "anonymous";
|
|
850
968
|
promises.push(
|
|
851
|
-
fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
969
|
+
fetchImage(url, 3e3, crossOrigin).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
852
970
|
})
|
|
853
971
|
);
|
|
854
972
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zumer/snapdom",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "snapDOM captures
|
|
3
|
+
"version": "1.7.1",
|
|
4
|
+
"description": "snapDOM captures HTML elements to images with exceptional speed and accuracy.",
|
|
5
5
|
"main": "dist/snapdom.js",
|
|
6
6
|
"module": "dist/snapdom.mjs",
|
|
7
7
|
"scripts": {
|
package/types/snapdom.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ declare module "@zumer/snapdom" {
|
|
|
9
9
|
filename?: string;
|
|
10
10
|
dpr?: number;
|
|
11
11
|
quality?: number;
|
|
12
|
+
crossOrigin?: (url: string) => "anonymous" | "use-credentials";
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface SnapResult {
|
|
@@ -51,6 +52,7 @@ declare module "@zumer/snapdom" {
|
|
|
51
52
|
root?: Document | HTMLElement,
|
|
52
53
|
options?: {
|
|
53
54
|
embedFonts?: boolean;
|
|
55
|
+
crossOrigin?: (url: string) => "anonymous" | "use-credentials";
|
|
54
56
|
}
|
|
55
57
|
): Promise<void>;
|
|
56
58
|
}
|