@zumer/snapdom 1.9.5 → 1.9.6-dev.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 +5 -2
- package/dist/snapdom.js +476 -303
- package/dist/snapdom.min.js +10 -10
- package/dist/snapdom.min.mjs +13 -13
- package/dist/snapdom.mjs +241 -198
- package/package.json +4 -1
- package/types/snapdom.d.ts +1 -1
package/dist/snapdom.mjs
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.
|
|
4
|
+
* v.1.9.6-dev.0
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
// src/core/cache.js
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
var cache = {
|
|
12
|
+
image: /* @__PURE__ */ new Map(),
|
|
13
|
+
background: /* @__PURE__ */ new Map(),
|
|
14
|
+
resource: /* @__PURE__ */ new Map(),
|
|
15
|
+
defaultStyle: /* @__PURE__ */ new Map(),
|
|
16
|
+
baseStyle: /* @__PURE__ */ new Map(),
|
|
17
|
+
computedStyle: /* @__PURE__ */ new WeakMap(),
|
|
18
|
+
font: /* @__PURE__ */ new Set(),
|
|
19
|
+
snapshot: /* @__PURE__ */ new WeakMap(),
|
|
20
|
+
snapshotKey: /* @__PURE__ */ new Map(),
|
|
21
|
+
preStyleMap: /* @__PURE__ */ new Map(),
|
|
22
|
+
preStyle: /* @__PURE__ */ new WeakMap(),
|
|
23
|
+
preNodeMap: /* @__PURE__ */ new Map(),
|
|
24
|
+
reset: resetCache
|
|
25
|
+
};
|
|
26
|
+
function resetCache() {
|
|
27
|
+
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
cache.snapshot = /* @__PURE__ */ new WeakMap();
|
|
29
|
+
cache.snapshotKey.clear();
|
|
30
|
+
cache.preStyleMap.clear();
|
|
31
|
+
cache.preStyle = /* @__PURE__ */ new WeakMap();
|
|
32
|
+
cache.preNodeMap.clear();
|
|
33
|
+
}
|
|
18
34
|
|
|
19
35
|
// src/utils/cssTools.js
|
|
20
36
|
var commonTags = [
|
|
@@ -62,13 +78,13 @@ function precacheCommonTags() {
|
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
function getDefaultStyleForTag(tagName) {
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
81
|
+
if (cache.defaultStyle.has(tagName)) {
|
|
82
|
+
return cache.defaultStyle.get(tagName);
|
|
67
83
|
}
|
|
68
84
|
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
|
|
69
85
|
if (skipTags.has(tagName)) {
|
|
70
86
|
const empty = {};
|
|
71
|
-
|
|
87
|
+
cache.defaultStyle.set(tagName, empty);
|
|
72
88
|
return empty;
|
|
73
89
|
}
|
|
74
90
|
let sandbox = document.getElementById("snapdom-sandbox");
|
|
@@ -92,7 +108,7 @@ function getDefaultStyleForTag(tagName) {
|
|
|
92
108
|
defaults[prop] = styles.getPropertyValue(prop);
|
|
93
109
|
}
|
|
94
110
|
sandbox.removeChild(el);
|
|
95
|
-
|
|
111
|
+
cache.defaultStyle.set(tagName, defaults);
|
|
96
112
|
return defaults;
|
|
97
113
|
}
|
|
98
114
|
function getStyleKey(snapshot, tagName, compress = false) {
|
|
@@ -128,7 +144,7 @@ function collectUsedTagNames(root) {
|
|
|
128
144
|
function generateDedupedBaseCSS(usedTagNames) {
|
|
129
145
|
const groups = /* @__PURE__ */ new Map();
|
|
130
146
|
for (let tagName of usedTagNames) {
|
|
131
|
-
const styles =
|
|
147
|
+
const styles = cache.defaultStyle.get(tagName);
|
|
132
148
|
if (!styles) continue;
|
|
133
149
|
const key = Object.entries(styles).map(([k, v]) => `${k}:${v};`).sort().join("");
|
|
134
150
|
if (!groups.has(key)) {
|
|
@@ -143,8 +159,8 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
143
159
|
}
|
|
144
160
|
return css;
|
|
145
161
|
}
|
|
146
|
-
function generateCSSClasses(
|
|
147
|
-
const keySet = new Set(
|
|
162
|
+
function generateCSSClasses() {
|
|
163
|
+
const keySet = new Set(cache.preStyleMap.values());
|
|
148
164
|
const classMap = /* @__PURE__ */ new Map();
|
|
149
165
|
let counter = 1;
|
|
150
166
|
for (const key of keySet) {
|
|
@@ -155,16 +171,15 @@ function generateCSSClasses(styleMap) {
|
|
|
155
171
|
|
|
156
172
|
// src/utils/helpers.js
|
|
157
173
|
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
158
|
-
const
|
|
159
|
-
const rawUrl = match?.[1];
|
|
174
|
+
const rawUrl = extractURL(entry);
|
|
160
175
|
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
161
176
|
if (rawUrl) {
|
|
162
177
|
const encodedUrl = safeEncodeURI(rawUrl);
|
|
163
|
-
if (
|
|
164
|
-
return options.skipInline ? void 0 : `url(${
|
|
178
|
+
if (cache.background.has(encodedUrl)) {
|
|
179
|
+
return options.skipInline ? void 0 : `url(${cache.background.get(encodedUrl)})`;
|
|
165
180
|
} else {
|
|
166
181
|
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
167
|
-
|
|
182
|
+
cache.background.set(encodedUrl, dataUrl);
|
|
168
183
|
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
169
184
|
}
|
|
170
185
|
}
|
|
@@ -185,10 +200,10 @@ function getStyle(el, pseudo = null) {
|
|
|
185
200
|
if (!(el instanceof Element)) {
|
|
186
201
|
return window.getComputedStyle(el, pseudo);
|
|
187
202
|
}
|
|
188
|
-
let map =
|
|
203
|
+
let map = cache.computedStyle.get(el);
|
|
189
204
|
if (!map) {
|
|
190
205
|
map = /* @__PURE__ */ new Map();
|
|
191
|
-
|
|
206
|
+
cache.computedStyle.set(el, map);
|
|
192
207
|
}
|
|
193
208
|
if (!map.has(pseudo)) {
|
|
194
209
|
const st = window.getComputedStyle(el, pseudo);
|
|
@@ -262,13 +277,13 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
262
277
|
}
|
|
263
278
|
const crossOriginValue = getCrossOriginMode(src);
|
|
264
279
|
console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
|
|
265
|
-
if (
|
|
280
|
+
if (cache.image.has(src)) {
|
|
266
281
|
console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
|
|
267
|
-
return Promise.resolve(
|
|
282
|
+
return Promise.resolve(cache.image.get(src));
|
|
268
283
|
}
|
|
269
284
|
const isDataURI = src.startsWith("data:image/");
|
|
270
285
|
if (isDataURI) {
|
|
271
|
-
|
|
286
|
+
cache.image.set(src, src);
|
|
272
287
|
return Promise.resolve(src);
|
|
273
288
|
}
|
|
274
289
|
const isSVG = /\.svg(\?.*)?$/i.test(src);
|
|
@@ -281,7 +296,7 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
281
296
|
});
|
|
282
297
|
const svgText = await response.text();
|
|
283
298
|
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
284
|
-
|
|
299
|
+
cache.image.set(src, encoded);
|
|
285
300
|
return encoded;
|
|
286
301
|
} catch {
|
|
287
302
|
return fetchWithFallback(src);
|
|
@@ -305,12 +320,12 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
305
320
|
const ctx = canvas.getContext("2d");
|
|
306
321
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
307
322
|
const dataURL = canvas.toDataURL("image/png");
|
|
308
|
-
|
|
323
|
+
cache.image.set(src, dataURL);
|
|
309
324
|
resolve(dataURL);
|
|
310
325
|
} catch {
|
|
311
326
|
try {
|
|
312
327
|
const fallbackDataURL = await fetchWithFallback(src);
|
|
313
|
-
|
|
328
|
+
cache.image.set(src, fallbackDataURL);
|
|
314
329
|
resolve(fallbackDataURL);
|
|
315
330
|
} catch (e) {
|
|
316
331
|
reject(e);
|
|
@@ -322,7 +337,7 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
322
337
|
console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
|
|
323
338
|
try {
|
|
324
339
|
const fallbackDataURL = await fetchWithFallback(src);
|
|
325
|
-
|
|
340
|
+
cache.image.set(src, fallbackDataURL);
|
|
326
341
|
resolve(fallbackDataURL);
|
|
327
342
|
} catch (e) {
|
|
328
343
|
reject(e);
|
|
@@ -386,8 +401,6 @@ function splitBackgroundImage(bg) {
|
|
|
386
401
|
}
|
|
387
402
|
|
|
388
403
|
// src/modules/styles.js
|
|
389
|
-
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
390
|
-
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
391
404
|
function snapshotComputedStyleFull(style) {
|
|
392
405
|
const result = {};
|
|
393
406
|
for (let i = 0; i < style.length; i++) {
|
|
@@ -400,147 +413,182 @@ function snapshotComputedStyleFull(style) {
|
|
|
400
413
|
}
|
|
401
414
|
return result;
|
|
402
415
|
}
|
|
403
|
-
function inlineAllStyles(source, clone,
|
|
416
|
+
function inlineAllStyles(source, clone, compress) {
|
|
404
417
|
if (source.tagName === "STYLE") return;
|
|
405
|
-
if (!cache.has(source)) {
|
|
406
|
-
cache.set(source, getStyle(source));
|
|
418
|
+
if (!cache.preStyle.has(source)) {
|
|
419
|
+
cache.preStyle.set(source, getStyle(source));
|
|
407
420
|
}
|
|
408
|
-
const style = cache.get(source);
|
|
409
|
-
if (!
|
|
421
|
+
const style = cache.preStyle.get(source);
|
|
422
|
+
if (!cache.snapshot.has(source)) {
|
|
410
423
|
const snapshot2 = snapshotComputedStyleFull(style);
|
|
411
|
-
|
|
424
|
+
cache.snapshot.set(source, snapshot2);
|
|
412
425
|
}
|
|
413
|
-
const snapshot =
|
|
426
|
+
const snapshot = cache.snapshot.get(source);
|
|
414
427
|
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
415
|
-
if (
|
|
416
|
-
|
|
428
|
+
if (cache.snapshotKey.has(hash)) {
|
|
429
|
+
cache.preStyleMap.set(clone, cache.snapshotKey.get(hash));
|
|
417
430
|
return;
|
|
418
431
|
}
|
|
419
432
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
420
433
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
421
|
-
|
|
422
|
-
|
|
434
|
+
cache.snapshotKey.set(hash, key);
|
|
435
|
+
cache.preStyleMap.set(clone, key);
|
|
423
436
|
}
|
|
424
437
|
|
|
425
438
|
// src/core/clone.js
|
|
426
439
|
function isSlotElement(node) {
|
|
427
440
|
return node.nodeType === Node.ELEMENT_NODE && node.tagName === "SLOT";
|
|
428
441
|
}
|
|
429
|
-
function deepClone(node,
|
|
430
|
-
if (node
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
function deepClone(node, compress, options = {}, originalRoot) {
|
|
443
|
+
if (!node) {
|
|
444
|
+
throw new Error("Invalid node: node is null or undefined");
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
448
|
+
return node.cloneNode(true);
|
|
449
|
+
}
|
|
450
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
451
|
+
return node.cloneNode(true);
|
|
452
|
+
}
|
|
453
|
+
if (node.getAttribute("data-capture") === "exclude") {
|
|
454
|
+
const spacer = document.createElement("div");
|
|
455
|
+
const rect = node.getBoundingClientRect();
|
|
456
|
+
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
457
|
+
return spacer;
|
|
458
|
+
}
|
|
459
|
+
if (options.exclude && Array.isArray(options.exclude)) {
|
|
460
|
+
for (const selector of options.exclude) {
|
|
461
|
+
try {
|
|
462
|
+
if (node.matches?.(selector)) {
|
|
463
|
+
const spacer = document.createElement("div");
|
|
464
|
+
const rect = node.getBoundingClientRect();
|
|
465
|
+
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
466
|
+
return spacer;
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.warn(`Invalid selector in exclude option: ${selector}`, err);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (typeof options.filter === "function") {
|
|
442
474
|
try {
|
|
443
|
-
if (
|
|
475
|
+
if (!options.filter(node, originalRoot || node)) {
|
|
444
476
|
const spacer = document.createElement("div");
|
|
445
477
|
const rect = node.getBoundingClientRect();
|
|
446
478
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
447
479
|
return spacer;
|
|
448
480
|
}
|
|
449
481
|
} catch (err) {
|
|
450
|
-
console.warn(
|
|
482
|
+
console.warn("Error in filter function:", err);
|
|
451
483
|
}
|
|
452
484
|
}
|
|
453
|
-
|
|
454
|
-
|
|
485
|
+
if (node.tagName === "IFRAME") {
|
|
486
|
+
const fallback = document.createElement("div");
|
|
487
|
+
fallback.textContent = "";
|
|
488
|
+
fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background-image: 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;`;
|
|
489
|
+
return fallback;
|
|
490
|
+
}
|
|
491
|
+
if (node.getAttribute("data-capture") === "placeholder") {
|
|
492
|
+
const clone2 = node.cloneNode(false);
|
|
493
|
+
cache.preNodeMap.set(clone2, node);
|
|
494
|
+
inlineAllStyles(node, clone2, compress);
|
|
495
|
+
const placeholder = document.createElement("div");
|
|
496
|
+
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
497
|
+
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
|
|
498
|
+
clone2.appendChild(placeholder);
|
|
499
|
+
return clone2;
|
|
500
|
+
}
|
|
501
|
+
if (node.tagName === "CANVAS") {
|
|
502
|
+
const dataURL = node.toDataURL();
|
|
503
|
+
const img = document.createElement("img");
|
|
504
|
+
img.src = dataURL;
|
|
505
|
+
img.width = node.width;
|
|
506
|
+
img.height = node.height;
|
|
507
|
+
img.style.display = "inline-block";
|
|
508
|
+
img.style.width = node.style.width || `${node.width}px`;
|
|
509
|
+
img.style.height = node.style.height || `${node.height}px`;
|
|
510
|
+
return img;
|
|
511
|
+
}
|
|
512
|
+
let clone;
|
|
455
513
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
460
|
-
return spacer;
|
|
514
|
+
clone = node.cloneNode(false);
|
|
515
|
+
if (!clone) {
|
|
516
|
+
throw new Error("Failed to clone node: cloneNode returned null");
|
|
461
517
|
}
|
|
518
|
+
cache.preNodeMap.set(clone, node);
|
|
462
519
|
} catch (err) {
|
|
463
|
-
console.
|
|
520
|
+
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
521
|
+
throw err;
|
|
464
522
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
if (node.getAttribute("data-capture") === "placeholder") {
|
|
473
|
-
const clone2 = node.cloneNode(false);
|
|
474
|
-
nodeMap.set(clone2, node);
|
|
475
|
-
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
476
|
-
const placeholder = document.createElement("div");
|
|
477
|
-
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
478
|
-
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
|
|
479
|
-
clone2.appendChild(placeholder);
|
|
480
|
-
return clone2;
|
|
481
|
-
}
|
|
482
|
-
if (node.tagName === "CANVAS") {
|
|
483
|
-
const dataURL = node.toDataURL();
|
|
484
|
-
const img = document.createElement("img");
|
|
485
|
-
img.src = dataURL;
|
|
486
|
-
img.width = node.width;
|
|
487
|
-
img.height = node.height;
|
|
488
|
-
img.style.display = "inline-block";
|
|
489
|
-
img.style.width = node.style.width || `${node.width}px`;
|
|
490
|
-
img.style.height = node.style.height || `${node.height}px`;
|
|
491
|
-
return img;
|
|
492
|
-
}
|
|
493
|
-
const clone = node.cloneNode(false);
|
|
494
|
-
nodeMap.set(clone, node);
|
|
495
|
-
if (node instanceof HTMLInputElement) {
|
|
496
|
-
clone.value = node.value;
|
|
497
|
-
clone.setAttribute("value", node.value);
|
|
498
|
-
if (node.checked !== void 0) {
|
|
499
|
-
clone.checked = node.checked;
|
|
500
|
-
if (node.checked) clone.setAttribute("checked", "");
|
|
501
|
-
}
|
|
502
|
-
} else if (node instanceof HTMLTextAreaElement) {
|
|
503
|
-
const rect = node.getBoundingClientRect();
|
|
504
|
-
clone.textContent = node.value;
|
|
505
|
-
clone.style.width = `${rect.width}px`;
|
|
506
|
-
clone.style.height = `${rect.height}px`;
|
|
507
|
-
} else if (node instanceof HTMLSelectElement) {
|
|
508
|
-
clone.value = node.value;
|
|
509
|
-
Array.from(clone.options).forEach((opt) => {
|
|
510
|
-
if (opt.value === node.value) {
|
|
511
|
-
opt.setAttribute("selected", "");
|
|
512
|
-
} else {
|
|
513
|
-
opt.removeAttribute("selected");
|
|
523
|
+
if (node instanceof HTMLInputElement) {
|
|
524
|
+
clone.value = node.value;
|
|
525
|
+
clone.setAttribute("value", node.value);
|
|
526
|
+
if (node.checked !== void 0) {
|
|
527
|
+
clone.checked = node.checked;
|
|
528
|
+
if (node.checked) clone.setAttribute("checked", "");
|
|
514
529
|
}
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
|
|
530
|
-
for (const child of baseChildren) {
|
|
531
|
-
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
532
|
-
if (clonedChild) clone.appendChild(clonedChild);
|
|
530
|
+
} else if (node instanceof HTMLTextAreaElement) {
|
|
531
|
+
const rect = node.getBoundingClientRect();
|
|
532
|
+
clone.textContent = node.value;
|
|
533
|
+
clone.style.width = `${rect.width}px`;
|
|
534
|
+
clone.style.height = `${rect.height}px`;
|
|
535
|
+
} else if (node instanceof HTMLSelectElement) {
|
|
536
|
+
clone.value = node.value;
|
|
537
|
+
Array.from(clone.options).forEach((opt) => {
|
|
538
|
+
if (opt.value === node.value) {
|
|
539
|
+
opt.setAttribute("selected", "");
|
|
540
|
+
} else {
|
|
541
|
+
opt.removeAttribute("selected");
|
|
542
|
+
}
|
|
543
|
+
});
|
|
533
544
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
545
|
+
inlineAllStyles(node, clone, compress);
|
|
546
|
+
try {
|
|
547
|
+
if (isSlotElement(node)) {
|
|
548
|
+
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
549
|
+
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
550
|
+
const fragment = document.createDocumentFragment();
|
|
551
|
+
for (const child of nodesToClone) {
|
|
552
|
+
try {
|
|
553
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
554
|
+
if (clonedChild) fragment.appendChild(clonedChild);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.warn("[Snapdom] Failed to clone slot content:", child, err);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return fragment;
|
|
560
|
+
} else {
|
|
561
|
+
const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
|
|
562
|
+
for (const child of baseChildren) {
|
|
563
|
+
try {
|
|
564
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
565
|
+
if (clonedChild) clone.appendChild(clonedChild);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
console.warn("[Snapdom] Failed to clone child node:", child, err);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (node.shadowRoot && node.childNodes.length > 0 && !node.shadowRoot.querySelector("slot")) {
|
|
571
|
+
const lightDomContent = document.createDocumentFragment();
|
|
572
|
+
for (const child of node.childNodes) {
|
|
573
|
+
try {
|
|
574
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
575
|
+
if (clonedChild) lightDomContent.appendChild(clonedChild);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
console.warn("[Snapdom] Failed to clone light DOM child:", child, err);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
clone.appendChild(lightDomContent);
|
|
581
|
+
}
|
|
539
582
|
}
|
|
540
|
-
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.error("[Snapdom] Error cloning children:", err);
|
|
585
|
+
throw err;
|
|
541
586
|
}
|
|
587
|
+
return clone;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error("[Snapdom] Error in deepClone:", error);
|
|
590
|
+
throw error;
|
|
542
591
|
}
|
|
543
|
-
return clone;
|
|
544
592
|
}
|
|
545
593
|
|
|
546
594
|
// src/modules/iconFonts.js
|
|
@@ -620,14 +668,14 @@ function injectLinkIfMissing(href) {
|
|
|
620
668
|
});
|
|
621
669
|
}
|
|
622
670
|
async function embedCustomFonts({ preCached = false } = {}) {
|
|
623
|
-
if (
|
|
671
|
+
if (cache.resource.has("fonts-embed-css")) {
|
|
624
672
|
if (preCached) {
|
|
625
673
|
const style = document.createElement("style");
|
|
626
674
|
style.setAttribute("data-snapdom", "embedFonts");
|
|
627
|
-
style.textContent =
|
|
675
|
+
style.textContent = cache.resource.get("fonts-embed-css");
|
|
628
676
|
document.head.appendChild(style);
|
|
629
677
|
}
|
|
630
|
-
return
|
|
678
|
+
return cache.resource.get("fonts-embed-css");
|
|
631
679
|
}
|
|
632
680
|
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
633
681
|
const styleImports = [];
|
|
@@ -660,11 +708,11 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
660
708
|
url = new URL(url, link.href).href;
|
|
661
709
|
}
|
|
662
710
|
if (isIconFont(url)) return null;
|
|
663
|
-
if (
|
|
664
|
-
|
|
665
|
-
return { original: match[0], inlined: `url(${
|
|
711
|
+
if (cache.resource.has(url)) {
|
|
712
|
+
cache.font.add(url);
|
|
713
|
+
return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
|
|
666
714
|
}
|
|
667
|
-
if (
|
|
715
|
+
if (cache.font.has(url)) return null;
|
|
668
716
|
try {
|
|
669
717
|
const fontRes = await fetch(url);
|
|
670
718
|
const blob = await fontRes.blob();
|
|
@@ -673,8 +721,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
673
721
|
reader.onload = () => resolve(reader.result);
|
|
674
722
|
reader.readAsDataURL(blob);
|
|
675
723
|
});
|
|
676
|
-
|
|
677
|
-
|
|
724
|
+
cache.resource.set(url, b64);
|
|
725
|
+
cache.font.add(url);
|
|
678
726
|
return { original: match[0], inlined: `url(${b64})` };
|
|
679
727
|
} catch (e) {
|
|
680
728
|
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
@@ -710,12 +758,12 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
710
758
|
url = new URL(url, sheet.href || location.href).href;
|
|
711
759
|
}
|
|
712
760
|
if (isIconFont(url)) continue;
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${
|
|
761
|
+
if (cache.resource.has(url)) {
|
|
762
|
+
cache.font.add(url);
|
|
763
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
716
764
|
continue;
|
|
717
765
|
}
|
|
718
|
-
if (
|
|
766
|
+
if (cache.font.has(url)) continue;
|
|
719
767
|
try {
|
|
720
768
|
const res = await fetch(url);
|
|
721
769
|
const blob = await res.blob();
|
|
@@ -724,8 +772,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
724
772
|
reader.onload = () => resolve(reader.result);
|
|
725
773
|
reader.readAsDataURL(blob);
|
|
726
774
|
});
|
|
727
|
-
|
|
728
|
-
|
|
775
|
+
cache.resource.set(url, b64);
|
|
776
|
+
cache.font.add(url);
|
|
729
777
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
730
778
|
} catch (e) {
|
|
731
779
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
@@ -750,10 +798,10 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
750
798
|
if (isIconFont(font.family)) continue;
|
|
751
799
|
let b64 = font._snapdomSrc;
|
|
752
800
|
if (!b64.startsWith("data:")) {
|
|
753
|
-
if (
|
|
754
|
-
b64 =
|
|
755
|
-
|
|
756
|
-
} else if (!
|
|
801
|
+
if (cache.resource.has(font._snapdomSrc)) {
|
|
802
|
+
b64 = cache.resource.get(font._snapdomSrc);
|
|
803
|
+
cache.font.add(font._snapdomSrc);
|
|
804
|
+
} else if (!cache.font.has(font._snapdomSrc)) {
|
|
757
805
|
try {
|
|
758
806
|
const res = await fetch(font._snapdomSrc);
|
|
759
807
|
const blob = await res.blob();
|
|
@@ -762,8 +810,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
762
810
|
reader.onload = () => resolve(reader.result);
|
|
763
811
|
reader.readAsDataURL(blob);
|
|
764
812
|
});
|
|
765
|
-
|
|
766
|
-
|
|
813
|
+
cache.resource.set(font._snapdomSrc, b64);
|
|
814
|
+
cache.font.add(font._snapdomSrc);
|
|
767
815
|
} catch (e) {
|
|
768
816
|
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
769
817
|
continue;
|
|
@@ -780,7 +828,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
780
828
|
}
|
|
781
829
|
}
|
|
782
830
|
if (finalCSS) {
|
|
783
|
-
|
|
831
|
+
cache.resource.set("fonts-embed-css", finalCSS);
|
|
784
832
|
if (preCached) {
|
|
785
833
|
const style = document.createElement("style");
|
|
786
834
|
style.setAttribute("data-snapdom", "embedFonts");
|
|
@@ -792,7 +840,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
792
840
|
}
|
|
793
841
|
|
|
794
842
|
// src/modules/pseudo.js
|
|
795
|
-
async function inlinePseudoElements(source, clone,
|
|
843
|
+
async function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
|
|
796
844
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
797
845
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
798
846
|
try {
|
|
@@ -816,7 +864,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
816
864
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
817
865
|
const snapshot = snapshotComputedStyle(style);
|
|
818
866
|
const key = getStyleKey(snapshot, "span", compress);
|
|
819
|
-
|
|
867
|
+
cache.preStyleMap.set(span, key);
|
|
820
868
|
const restNode = document.createTextNode(rest);
|
|
821
869
|
clone.replaceChild(restNode, textNode);
|
|
822
870
|
clone.insertBefore(span, restNode);
|
|
@@ -837,7 +885,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
837
885
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
838
886
|
const snapshot = snapshotComputedStyle(style);
|
|
839
887
|
const key = getStyleKey(snapshot, "span", compress);
|
|
840
|
-
|
|
888
|
+
cache.preStyleMap.set(pseudoEl, key);
|
|
841
889
|
const isIconFont2 = isIconFont(fontFamily);
|
|
842
890
|
const cleanContent = parseContent(content);
|
|
843
891
|
if (isIconFont2 && cleanContent.length === 1) {
|
|
@@ -888,8 +936,6 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
888
936
|
await inlinePseudoElements(
|
|
889
937
|
sChildren[i],
|
|
890
938
|
cChildren[i],
|
|
891
|
-
styleMap,
|
|
892
|
-
styleCache,
|
|
893
939
|
compress,
|
|
894
940
|
embedFonts,
|
|
895
941
|
useProxy
|
|
@@ -929,18 +975,16 @@ function inlineExternalDef(root) {
|
|
|
929
975
|
|
|
930
976
|
// src/core/prepare.js
|
|
931
977
|
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
932
|
-
const styleMap = /* @__PURE__ */ new Map();
|
|
933
|
-
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
934
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
935
978
|
let clone;
|
|
979
|
+
let classCSS = "";
|
|
936
980
|
try {
|
|
937
|
-
clone = deepClone(element,
|
|
981
|
+
clone = deepClone(element, compress, options, element);
|
|
938
982
|
} catch (e) {
|
|
939
983
|
console.warn("deepClone failed:", e);
|
|
940
984
|
throw e;
|
|
941
985
|
}
|
|
942
986
|
try {
|
|
943
|
-
await inlinePseudoElements(element, clone,
|
|
987
|
+
await inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
|
|
944
988
|
} catch (e) {
|
|
945
989
|
console.warn("inlinePseudoElements failed:", e);
|
|
946
990
|
}
|
|
@@ -949,11 +993,10 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
949
993
|
} catch (e) {
|
|
950
994
|
console.warn("inlineExternalDef failed:", e);
|
|
951
995
|
}
|
|
952
|
-
let classCSS = "";
|
|
953
996
|
if (compress) {
|
|
954
|
-
const keyToClass = generateCSSClasses(
|
|
997
|
+
const keyToClass = generateCSSClasses();
|
|
955
998
|
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
956
|
-
for (const [node, key] of
|
|
999
|
+
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
957
1000
|
if (node.tagName === "STYLE") continue;
|
|
958
1001
|
const className = keyToClass.get(key);
|
|
959
1002
|
if (className) node.classList.add(className);
|
|
@@ -962,12 +1005,12 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
962
1005
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
963
1006
|
}
|
|
964
1007
|
} else {
|
|
965
|
-
for (const [node, key] of
|
|
1008
|
+
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
966
1009
|
if (node.tagName === "STYLE") continue;
|
|
967
1010
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
968
1011
|
}
|
|
969
1012
|
}
|
|
970
|
-
for (const [cloneNode, originalNode] of
|
|
1013
|
+
for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
|
|
971
1014
|
const scrollX = originalNode.scrollLeft;
|
|
972
1015
|
const scrollY = originalNode.scrollTop;
|
|
973
1016
|
const hasScroll = scrollX || scrollY;
|
|
@@ -986,9 +1029,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
986
1029
|
cloneNode.appendChild(inner);
|
|
987
1030
|
}
|
|
988
1031
|
}
|
|
989
|
-
if (element ===
|
|
990
|
-
const computed =
|
|
991
|
-
|
|
1032
|
+
if (element === cache.preNodeMap.get(clone)) {
|
|
1033
|
+
const computed = cache.preStyle.get(element) || window.getComputedStyle(element);
|
|
1034
|
+
cache.preStyle.set(element, computed);
|
|
992
1035
|
const transform = stripTranslate(computed.transform);
|
|
993
1036
|
clone.style.margin = "0";
|
|
994
1037
|
clone.style.position = "static";
|
|
@@ -1001,13 +1044,13 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1001
1044
|
clone.style.clear = "none";
|
|
1002
1045
|
clone.style.transform = transform || "";
|
|
1003
1046
|
}
|
|
1004
|
-
for (const [cloneNode, originalNode] of
|
|
1047
|
+
for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
|
|
1005
1048
|
if (originalNode.tagName === "PRE") {
|
|
1006
1049
|
cloneNode.style.marginTop = "0";
|
|
1007
1050
|
cloneNode.style.marginBlockStart = "0";
|
|
1008
1051
|
}
|
|
1009
1052
|
}
|
|
1010
|
-
return { clone, classCSS
|
|
1053
|
+
return { clone, classCSS };
|
|
1011
1054
|
}
|
|
1012
1055
|
|
|
1013
1056
|
// src/modules/images.js
|
|
@@ -1034,7 +1077,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
1034
1077
|
}
|
|
1035
1078
|
|
|
1036
1079
|
// src/modules/background.js
|
|
1037
|
-
async function inlineBackgroundImages(source, clone,
|
|
1080
|
+
async function inlineBackgroundImages(source, clone, options = {}) {
|
|
1038
1081
|
const queue = [[source, clone]];
|
|
1039
1082
|
const imageProps = [
|
|
1040
1083
|
"background-image",
|
|
@@ -1048,8 +1091,8 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
1048
1091
|
];
|
|
1049
1092
|
while (queue.length) {
|
|
1050
1093
|
const [srcNode, cloneNode] = queue.shift();
|
|
1051
|
-
const style =
|
|
1052
|
-
if (!
|
|
1094
|
+
const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
|
|
1095
|
+
if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
|
|
1053
1096
|
for (const prop of imageProps) {
|
|
1054
1097
|
const val = style.getPropertyValue(prop);
|
|
1055
1098
|
if (!val || val === "none") continue;
|
|
@@ -1076,13 +1119,14 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
1076
1119
|
// src/core/capture.js
|
|
1077
1120
|
async function captureDOM(element, options = {}) {
|
|
1078
1121
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1122
|
+
cache.reset();
|
|
1079
1123
|
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
1080
|
-
let clone, classCSS
|
|
1124
|
+
let clone, classCSS;
|
|
1081
1125
|
let fontsCSS = "";
|
|
1082
1126
|
let baseCSS = "";
|
|
1083
1127
|
let dataURL;
|
|
1084
1128
|
let svgString;
|
|
1085
|
-
({ clone, classCSS
|
|
1129
|
+
({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));
|
|
1086
1130
|
await new Promise((resolve) => {
|
|
1087
1131
|
idle(async () => {
|
|
1088
1132
|
await inlineImages(clone, options);
|
|
@@ -1091,7 +1135,7 @@ async function captureDOM(element, options = {}) {
|
|
|
1091
1135
|
});
|
|
1092
1136
|
await new Promise((resolve) => {
|
|
1093
1137
|
idle(async () => {
|
|
1094
|
-
await inlineBackgroundImages(element, clone,
|
|
1138
|
+
await inlineBackgroundImages(element, clone, options);
|
|
1095
1139
|
resolve();
|
|
1096
1140
|
}, { fast });
|
|
1097
1141
|
});
|
|
@@ -1106,13 +1150,13 @@ async function captureDOM(element, options = {}) {
|
|
|
1106
1150
|
if (compress) {
|
|
1107
1151
|
const usedTags = collectUsedTagNames(clone).sort();
|
|
1108
1152
|
const tagKey = usedTags.join(",");
|
|
1109
|
-
if (
|
|
1110
|
-
baseCSS =
|
|
1153
|
+
if (cache.baseStyle.has(tagKey)) {
|
|
1154
|
+
baseCSS = cache.baseStyle.get(tagKey);
|
|
1111
1155
|
} else {
|
|
1112
1156
|
await new Promise((resolve) => {
|
|
1113
1157
|
idle(() => {
|
|
1114
1158
|
baseCSS = generateDedupedBaseCSS(usedTags);
|
|
1115
|
-
|
|
1159
|
+
cache.baseStyle.set(tagKey, baseCSS);
|
|
1116
1160
|
resolve();
|
|
1117
1161
|
}, { fast });
|
|
1118
1162
|
});
|
|
@@ -1239,8 +1283,10 @@ async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
|
|
|
1239
1283
|
ctx.drawImage(baseCanvas, 0, 0);
|
|
1240
1284
|
return temp;
|
|
1241
1285
|
}
|
|
1242
|
-
async function toRasterImg(url, { dpr = 1, scale = 1, backgroundColor
|
|
1243
|
-
const
|
|
1286
|
+
async function toRasterImg(url, { dpr = 1, scale = 1, backgroundColor, quality }, format = "png") {
|
|
1287
|
+
const defaultBg = ["jpg", "jpeg", "webp"].includes(format) ? "#fff" : void 0;
|
|
1288
|
+
const finalBg = backgroundColor ?? defaultBg;
|
|
1289
|
+
const canvas = await createBackground(url, { dpr, scale }, finalBg);
|
|
1244
1290
|
const img = new Image();
|
|
1245
1291
|
img.src = canvas.toDataURL(`image/${format}`, quality);
|
|
1246
1292
|
await img.decode();
|
|
@@ -1321,10 +1367,7 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1321
1367
|
async function preCache(root = document, options = {}) {
|
|
1322
1368
|
const { embedFonts = true, reset = false } = options;
|
|
1323
1369
|
if (reset) {
|
|
1324
|
-
|
|
1325
|
-
bgCache.clear();
|
|
1326
|
-
resourceCache.clear();
|
|
1327
|
-
baseCSSCache.clear();
|
|
1370
|
+
cache.reset();
|
|
1328
1371
|
return;
|
|
1329
1372
|
}
|
|
1330
1373
|
await document.fonts.ready;
|
|
@@ -1337,9 +1380,9 @@ async function preCache(root = document, options = {}) {
|
|
|
1337
1380
|
const promises = [];
|
|
1338
1381
|
for (const img of imgEls) {
|
|
1339
1382
|
const src = img.src;
|
|
1340
|
-
if (!
|
|
1383
|
+
if (!cache.image.has(src)) {
|
|
1341
1384
|
promises.push(
|
|
1342
|
-
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) =>
|
|
1385
|
+
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => cache.image.set(src, dataURL)).catch(() => {
|
|
1343
1386
|
})
|
|
1344
1387
|
);
|
|
1345
1388
|
}
|