@zumer/snapdom 1.9.3 → 1.9.6-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -34
- package/dist/snapdom.js +165 -143
- package/dist/snapdom.min.js +7 -7
- package/dist/snapdom.min.mjs +10 -10
- package/dist/snapdom.mjs +163 -141
- package/package.json +1 -1
- package/types/snapdom.d.ts +1 -0
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,17 +171,15 @@ function generateCSSClasses(styleMap) {
|
|
|
155
171
|
|
|
156
172
|
// src/utils/helpers.js
|
|
157
173
|
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
158
|
-
const
|
|
174
|
+
const rawUrl = extractURL(entry);
|
|
159
175
|
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
160
|
-
if (
|
|
161
|
-
const rawUrl = extractURL(entry);
|
|
162
|
-
if (!rawUrl) return entry;
|
|
176
|
+
if (rawUrl) {
|
|
163
177
|
const encodedUrl = safeEncodeURI(rawUrl);
|
|
164
|
-
if (
|
|
165
|
-
return options.skipInline ? void 0 : `url(${
|
|
178
|
+
if (cache.background.has(encodedUrl)) {
|
|
179
|
+
return options.skipInline ? void 0 : `url(${cache.background.get(encodedUrl)})`;
|
|
166
180
|
} else {
|
|
167
181
|
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
168
|
-
|
|
182
|
+
cache.background.set(encodedUrl, dataUrl);
|
|
169
183
|
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
170
184
|
}
|
|
171
185
|
}
|
|
@@ -186,10 +200,10 @@ function getStyle(el, pseudo = null) {
|
|
|
186
200
|
if (!(el instanceof Element)) {
|
|
187
201
|
return window.getComputedStyle(el, pseudo);
|
|
188
202
|
}
|
|
189
|
-
let map =
|
|
203
|
+
let map = cache.computedStyle.get(el);
|
|
190
204
|
if (!map) {
|
|
191
205
|
map = /* @__PURE__ */ new Map();
|
|
192
|
-
|
|
206
|
+
cache.computedStyle.set(el, map);
|
|
193
207
|
}
|
|
194
208
|
if (!map.has(pseudo)) {
|
|
195
209
|
const st = window.getComputedStyle(el, pseudo);
|
|
@@ -263,13 +277,13 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
263
277
|
}
|
|
264
278
|
const crossOriginValue = getCrossOriginMode(src);
|
|
265
279
|
console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
|
|
266
|
-
if (
|
|
280
|
+
if (cache.image.has(src)) {
|
|
267
281
|
console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
|
|
268
|
-
return Promise.resolve(
|
|
282
|
+
return Promise.resolve(cache.image.get(src));
|
|
269
283
|
}
|
|
270
284
|
const isDataURI = src.startsWith("data:image/");
|
|
271
285
|
if (isDataURI) {
|
|
272
|
-
|
|
286
|
+
cache.image.set(src, src);
|
|
273
287
|
return Promise.resolve(src);
|
|
274
288
|
}
|
|
275
289
|
const isSVG = /\.svg(\?.*)?$/i.test(src);
|
|
@@ -282,7 +296,7 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
282
296
|
});
|
|
283
297
|
const svgText = await response.text();
|
|
284
298
|
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
285
|
-
|
|
299
|
+
cache.image.set(src, encoded);
|
|
286
300
|
return encoded;
|
|
287
301
|
} catch {
|
|
288
302
|
return fetchWithFallback(src);
|
|
@@ -306,12 +320,12 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
306
320
|
const ctx = canvas.getContext("2d");
|
|
307
321
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
308
322
|
const dataURL = canvas.toDataURL("image/png");
|
|
309
|
-
|
|
323
|
+
cache.image.set(src, dataURL);
|
|
310
324
|
resolve(dataURL);
|
|
311
325
|
} catch {
|
|
312
326
|
try {
|
|
313
327
|
const fallbackDataURL = await fetchWithFallback(src);
|
|
314
|
-
|
|
328
|
+
cache.image.set(src, fallbackDataURL);
|
|
315
329
|
resolve(fallbackDataURL);
|
|
316
330
|
} catch (e) {
|
|
317
331
|
reject(e);
|
|
@@ -323,7 +337,7 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
323
337
|
console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
|
|
324
338
|
try {
|
|
325
339
|
const fallbackDataURL = await fetchWithFallback(src);
|
|
326
|
-
|
|
340
|
+
cache.image.set(src, fallbackDataURL);
|
|
327
341
|
resolve(fallbackDataURL);
|
|
328
342
|
} catch (e) {
|
|
329
343
|
reject(e);
|
|
@@ -387,8 +401,6 @@ function splitBackgroundImage(bg) {
|
|
|
387
401
|
}
|
|
388
402
|
|
|
389
403
|
// src/modules/styles.js
|
|
390
|
-
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
391
|
-
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
392
404
|
function snapshotComputedStyleFull(style) {
|
|
393
405
|
const result = {};
|
|
394
406
|
for (let i = 0; i < style.length; i++) {
|
|
@@ -401,35 +413,34 @@ function snapshotComputedStyleFull(style) {
|
|
|
401
413
|
}
|
|
402
414
|
return result;
|
|
403
415
|
}
|
|
404
|
-
function inlineAllStyles(source, clone,
|
|
416
|
+
function inlineAllStyles(source, clone, compress) {
|
|
405
417
|
if (source.tagName === "STYLE") return;
|
|
406
|
-
if (!cache.has(source)) {
|
|
407
|
-
cache.set(source, getStyle(source));
|
|
418
|
+
if (!cache.preStyle.has(source)) {
|
|
419
|
+
cache.preStyle.set(source, getStyle(source));
|
|
408
420
|
}
|
|
409
|
-
const style = cache.get(source);
|
|
410
|
-
if (!
|
|
421
|
+
const style = cache.preStyle.get(source);
|
|
422
|
+
if (!cache.snapshot.has(source)) {
|
|
411
423
|
const snapshot2 = snapshotComputedStyleFull(style);
|
|
412
|
-
|
|
424
|
+
cache.snapshot.set(source, snapshot2);
|
|
413
425
|
}
|
|
414
|
-
const snapshot =
|
|
426
|
+
const snapshot = cache.snapshot.get(source);
|
|
415
427
|
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
416
|
-
if (
|
|
417
|
-
|
|
428
|
+
if (cache.snapshotKey.has(hash)) {
|
|
429
|
+
cache.preStyleMap.set(clone, cache.snapshotKey.get(hash));
|
|
418
430
|
return;
|
|
419
431
|
}
|
|
420
432
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
421
433
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
422
|
-
|
|
423
|
-
|
|
434
|
+
cache.snapshotKey.set(hash, key);
|
|
435
|
+
cache.preStyleMap.set(clone, key);
|
|
424
436
|
}
|
|
425
437
|
|
|
426
438
|
// src/core/clone.js
|
|
427
|
-
function
|
|
439
|
+
function isSlotElement(node) {
|
|
440
|
+
return node.nodeType === Node.ELEMENT_NODE && node.tagName === "SLOT";
|
|
441
|
+
}
|
|
442
|
+
function deepClone(node, compress, options = {}, originalRoot) {
|
|
428
443
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
429
|
-
if (node.parentElement?.shadowRoot) {
|
|
430
|
-
const tag = node.parentElement.tagName.toLowerCase();
|
|
431
|
-
if (customElements.get(tag)) return null;
|
|
432
|
-
}
|
|
433
444
|
return node.cloneNode(true);
|
|
434
445
|
}
|
|
435
446
|
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
@@ -439,10 +450,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
439
450
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
440
451
|
return spacer;
|
|
441
452
|
}
|
|
442
|
-
if (options.exclude && Array.isArray(options.exclude)
|
|
453
|
+
if (options.exclude && Array.isArray(options.exclude)) {
|
|
443
454
|
for (const selector of options.exclude) {
|
|
444
455
|
try {
|
|
445
|
-
if (node.matches
|
|
456
|
+
if (node.matches?.(selector)) {
|
|
446
457
|
const spacer = document.createElement("div");
|
|
447
458
|
const rect = node.getBoundingClientRect();
|
|
448
459
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
@@ -473,8 +484,8 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
473
484
|
}
|
|
474
485
|
if (node.getAttribute("data-capture") === "placeholder") {
|
|
475
486
|
const clone2 = node.cloneNode(false);
|
|
476
|
-
|
|
477
|
-
inlineAllStyles(node, clone2,
|
|
487
|
+
cache.preNodeMap.set(clone2, node);
|
|
488
|
+
inlineAllStyles(node, clone2, compress);
|
|
478
489
|
const placeholder = document.createElement("div");
|
|
479
490
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
480
491
|
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
|
|
@@ -493,7 +504,7 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
493
504
|
return img;
|
|
494
505
|
}
|
|
495
506
|
const clone = node.cloneNode(false);
|
|
496
|
-
|
|
507
|
+
cache.preNodeMap.set(clone, node);
|
|
497
508
|
if (node instanceof HTMLInputElement) {
|
|
498
509
|
clone.value = node.value;
|
|
499
510
|
clone.setAttribute("value", node.value);
|
|
@@ -502,8 +513,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
502
513
|
if (node.checked) clone.setAttribute("checked", "");
|
|
503
514
|
}
|
|
504
515
|
} else if (node instanceof HTMLTextAreaElement) {
|
|
505
|
-
|
|
516
|
+
const rect = node.getBoundingClientRect();
|
|
506
517
|
clone.textContent = node.value;
|
|
518
|
+
clone.style.width = `${rect.width}px`;
|
|
519
|
+
clone.style.height = `${rect.height}px`;
|
|
507
520
|
} else if (node instanceof HTMLSelectElement) {
|
|
508
521
|
clone.value = node.value;
|
|
509
522
|
Array.from(clone.options).forEach((opt) => {
|
|
@@ -514,19 +527,31 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
514
527
|
}
|
|
515
528
|
});
|
|
516
529
|
}
|
|
517
|
-
inlineAllStyles(node, clone,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
+
inlineAllStyles(node, clone, compress);
|
|
531
|
+
if (isSlotElement(node)) {
|
|
532
|
+
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
533
|
+
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
534
|
+
const fragment = document.createDocumentFragment();
|
|
535
|
+
for (const child of nodesToClone) {
|
|
536
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
537
|
+
if (clonedChild) fragment.appendChild(clonedChild);
|
|
538
|
+
}
|
|
539
|
+
return fragment;
|
|
540
|
+
} else if (node instanceof HTMLTextAreaElement) {
|
|
541
|
+
} else {
|
|
542
|
+
const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
|
|
543
|
+
for (const child of baseChildren) {
|
|
544
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
545
|
+
if (clonedChild) clone.appendChild(clonedChild);
|
|
546
|
+
}
|
|
547
|
+
if (node.shadowRoot && node.childNodes.length > 0 && !node.shadowRoot.querySelector("slot")) {
|
|
548
|
+
const lightDomContent = document.createDocumentFragment();
|
|
549
|
+
for (const child of node.childNodes) {
|
|
550
|
+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
551
|
+
if (clonedChild) lightDomContent.appendChild(clonedChild);
|
|
552
|
+
}
|
|
553
|
+
clone.appendChild(lightDomContent);
|
|
554
|
+
}
|
|
530
555
|
}
|
|
531
556
|
return clone;
|
|
532
557
|
}
|
|
@@ -608,14 +633,14 @@ function injectLinkIfMissing(href) {
|
|
|
608
633
|
});
|
|
609
634
|
}
|
|
610
635
|
async function embedCustomFonts({ preCached = false } = {}) {
|
|
611
|
-
if (
|
|
636
|
+
if (cache.resource.has("fonts-embed-css")) {
|
|
612
637
|
if (preCached) {
|
|
613
638
|
const style = document.createElement("style");
|
|
614
639
|
style.setAttribute("data-snapdom", "embedFonts");
|
|
615
|
-
style.textContent =
|
|
640
|
+
style.textContent = cache.resource.get("fonts-embed-css");
|
|
616
641
|
document.head.appendChild(style);
|
|
617
642
|
}
|
|
618
|
-
return
|
|
643
|
+
return cache.resource.get("fonts-embed-css");
|
|
619
644
|
}
|
|
620
645
|
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
621
646
|
const styleImports = [];
|
|
@@ -648,11 +673,11 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
648
673
|
url = new URL(url, link.href).href;
|
|
649
674
|
}
|
|
650
675
|
if (isIconFont(url)) return null;
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
return { original: match[0], inlined: `url(${
|
|
676
|
+
if (cache.resource.has(url)) {
|
|
677
|
+
cache.font.add(url);
|
|
678
|
+
return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
|
|
654
679
|
}
|
|
655
|
-
if (
|
|
680
|
+
if (cache.font.has(url)) return null;
|
|
656
681
|
try {
|
|
657
682
|
const fontRes = await fetch(url);
|
|
658
683
|
const blob = await fontRes.blob();
|
|
@@ -661,8 +686,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
661
686
|
reader.onload = () => resolve(reader.result);
|
|
662
687
|
reader.readAsDataURL(blob);
|
|
663
688
|
});
|
|
664
|
-
|
|
665
|
-
|
|
689
|
+
cache.resource.set(url, b64);
|
|
690
|
+
cache.font.add(url);
|
|
666
691
|
return { original: match[0], inlined: `url(${b64})` };
|
|
667
692
|
} catch (e) {
|
|
668
693
|
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
@@ -698,12 +723,12 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
698
723
|
url = new URL(url, sheet.href || location.href).href;
|
|
699
724
|
}
|
|
700
725
|
if (isIconFont(url)) continue;
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${
|
|
726
|
+
if (cache.resource.has(url)) {
|
|
727
|
+
cache.font.add(url);
|
|
728
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
704
729
|
continue;
|
|
705
730
|
}
|
|
706
|
-
if (
|
|
731
|
+
if (cache.font.has(url)) continue;
|
|
707
732
|
try {
|
|
708
733
|
const res = await fetch(url);
|
|
709
734
|
const blob = await res.blob();
|
|
@@ -712,8 +737,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
712
737
|
reader.onload = () => resolve(reader.result);
|
|
713
738
|
reader.readAsDataURL(blob);
|
|
714
739
|
});
|
|
715
|
-
|
|
716
|
-
|
|
740
|
+
cache.resource.set(url, b64);
|
|
741
|
+
cache.font.add(url);
|
|
717
742
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
718
743
|
} catch (e) {
|
|
719
744
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
@@ -738,10 +763,10 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
738
763
|
if (isIconFont(font.family)) continue;
|
|
739
764
|
let b64 = font._snapdomSrc;
|
|
740
765
|
if (!b64.startsWith("data:")) {
|
|
741
|
-
if (
|
|
742
|
-
b64 =
|
|
743
|
-
|
|
744
|
-
} else if (!
|
|
766
|
+
if (cache.resource.has(font._snapdomSrc)) {
|
|
767
|
+
b64 = cache.resource.get(font._snapdomSrc);
|
|
768
|
+
cache.font.add(font._snapdomSrc);
|
|
769
|
+
} else if (!cache.font.has(font._snapdomSrc)) {
|
|
745
770
|
try {
|
|
746
771
|
const res = await fetch(font._snapdomSrc);
|
|
747
772
|
const blob = await res.blob();
|
|
@@ -750,8 +775,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
750
775
|
reader.onload = () => resolve(reader.result);
|
|
751
776
|
reader.readAsDataURL(blob);
|
|
752
777
|
});
|
|
753
|
-
|
|
754
|
-
|
|
778
|
+
cache.resource.set(font._snapdomSrc, b64);
|
|
779
|
+
cache.font.add(font._snapdomSrc);
|
|
755
780
|
} catch (e) {
|
|
756
781
|
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
757
782
|
continue;
|
|
@@ -768,7 +793,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
768
793
|
}
|
|
769
794
|
}
|
|
770
795
|
if (finalCSS) {
|
|
771
|
-
|
|
796
|
+
cache.resource.set("fonts-embed-css", finalCSS);
|
|
772
797
|
if (preCached) {
|
|
773
798
|
const style = document.createElement("style");
|
|
774
799
|
style.setAttribute("data-snapdom", "embedFonts");
|
|
@@ -780,7 +805,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
780
805
|
}
|
|
781
806
|
|
|
782
807
|
// src/modules/pseudo.js
|
|
783
|
-
async function inlinePseudoElements(source, clone,
|
|
808
|
+
async function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
|
|
784
809
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
785
810
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
786
811
|
try {
|
|
@@ -804,7 +829,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
804
829
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
805
830
|
const snapshot = snapshotComputedStyle(style);
|
|
806
831
|
const key = getStyleKey(snapshot, "span", compress);
|
|
807
|
-
|
|
832
|
+
cache.preStyleMap.set(span, key);
|
|
808
833
|
const restNode = document.createTextNode(rest);
|
|
809
834
|
clone.replaceChild(restNode, textNode);
|
|
810
835
|
clone.insertBefore(span, restNode);
|
|
@@ -825,7 +850,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
825
850
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
826
851
|
const snapshot = snapshotComputedStyle(style);
|
|
827
852
|
const key = getStyleKey(snapshot, "span", compress);
|
|
828
|
-
|
|
853
|
+
cache.preStyleMap.set(pseudoEl, key);
|
|
829
854
|
const isIconFont2 = isIconFont(fontFamily);
|
|
830
855
|
const cleanContent = parseContent(content);
|
|
831
856
|
if (isIconFont2 && cleanContent.length === 1) {
|
|
@@ -876,8 +901,6 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
876
901
|
await inlinePseudoElements(
|
|
877
902
|
sChildren[i],
|
|
878
903
|
cChildren[i],
|
|
879
|
-
styleMap,
|
|
880
|
-
styleCache,
|
|
881
904
|
compress,
|
|
882
905
|
embedFonts,
|
|
883
906
|
useProxy
|
|
@@ -917,18 +940,15 @@ function inlineExternalDef(root) {
|
|
|
917
940
|
|
|
918
941
|
// src/core/prepare.js
|
|
919
942
|
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
920
|
-
|
|
921
|
-
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
922
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
923
|
-
let clone;
|
|
943
|
+
let clone, classCSS;
|
|
924
944
|
try {
|
|
925
|
-
clone = deepClone(element,
|
|
945
|
+
clone = deepClone(element, compress, options, element);
|
|
926
946
|
} catch (e) {
|
|
927
947
|
console.warn("deepClone failed:", e);
|
|
928
948
|
throw e;
|
|
929
949
|
}
|
|
930
950
|
try {
|
|
931
|
-
await inlinePseudoElements(element, clone,
|
|
951
|
+
await inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
|
|
932
952
|
} catch (e) {
|
|
933
953
|
console.warn("inlinePseudoElements failed:", e);
|
|
934
954
|
}
|
|
@@ -937,11 +957,10 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
937
957
|
} catch (e) {
|
|
938
958
|
console.warn("inlineExternalDef failed:", e);
|
|
939
959
|
}
|
|
940
|
-
let classCSS = "";
|
|
941
960
|
if (compress) {
|
|
942
|
-
const keyToClass = generateCSSClasses(
|
|
961
|
+
const keyToClass = generateCSSClasses();
|
|
943
962
|
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
944
|
-
for (const [node, key] of
|
|
963
|
+
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
945
964
|
if (node.tagName === "STYLE") continue;
|
|
946
965
|
const className = keyToClass.get(key);
|
|
947
966
|
if (className) node.classList.add(className);
|
|
@@ -950,12 +969,12 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
950
969
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
951
970
|
}
|
|
952
971
|
} else {
|
|
953
|
-
for (const [node, key] of
|
|
972
|
+
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
954
973
|
if (node.tagName === "STYLE") continue;
|
|
955
974
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
956
975
|
}
|
|
957
976
|
}
|
|
958
|
-
for (const [cloneNode, originalNode] of
|
|
977
|
+
for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
|
|
959
978
|
const scrollX = originalNode.scrollLeft;
|
|
960
979
|
const scrollY = originalNode.scrollTop;
|
|
961
980
|
const hasScroll = scrollX || scrollY;
|
|
@@ -974,9 +993,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
974
993
|
cloneNode.appendChild(inner);
|
|
975
994
|
}
|
|
976
995
|
}
|
|
977
|
-
if (element ===
|
|
978
|
-
const computed =
|
|
979
|
-
|
|
996
|
+
if (element === cache.preNodeMap.get(clone)) {
|
|
997
|
+
const computed = cache.preStyle.get(element) || window.getComputedStyle(element);
|
|
998
|
+
cache.preStyle.set(element, computed);
|
|
980
999
|
const transform = stripTranslate(computed.transform);
|
|
981
1000
|
clone.style.margin = "0";
|
|
982
1001
|
clone.style.position = "static";
|
|
@@ -989,13 +1008,13 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
989
1008
|
clone.style.clear = "none";
|
|
990
1009
|
clone.style.transform = transform || "";
|
|
991
1010
|
}
|
|
992
|
-
for (const [cloneNode, originalNode] of
|
|
1011
|
+
for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
|
|
993
1012
|
if (originalNode.tagName === "PRE") {
|
|
994
1013
|
cloneNode.style.marginTop = "0";
|
|
995
1014
|
cloneNode.style.marginBlockStart = "0";
|
|
996
1015
|
}
|
|
997
1016
|
}
|
|
998
|
-
return { clone, classCSS
|
|
1017
|
+
return { clone, classCSS };
|
|
999
1018
|
}
|
|
1000
1019
|
|
|
1001
1020
|
// src/modules/images.js
|
|
@@ -1022,29 +1041,34 @@ async function inlineImages(clone, options = {}) {
|
|
|
1022
1041
|
}
|
|
1023
1042
|
|
|
1024
1043
|
// src/modules/background.js
|
|
1025
|
-
async function inlineBackgroundImages(source, clone,
|
|
1044
|
+
async function inlineBackgroundImages(source, clone, options = {}) {
|
|
1026
1045
|
const queue = [[source, clone]];
|
|
1046
|
+
const imageProps = [
|
|
1047
|
+
"background-image",
|
|
1048
|
+
"mask",
|
|
1049
|
+
"mask-image",
|
|
1050
|
+
"-webkit-mask-image",
|
|
1051
|
+
"mask-source",
|
|
1052
|
+
"mask-box-image-source",
|
|
1053
|
+
"mask-border-source",
|
|
1054
|
+
"-webkit-mask-box-image-source"
|
|
1055
|
+
];
|
|
1027
1056
|
while (queue.length) {
|
|
1028
1057
|
const [srcNode, cloneNode] = queue.shift();
|
|
1029
|
-
const style =
|
|
1030
|
-
if (!
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1058
|
+
const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
|
|
1059
|
+
if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
|
|
1060
|
+
for (const prop of imageProps) {
|
|
1061
|
+
const val = style.getPropertyValue(prop);
|
|
1062
|
+
if (!val || val === "none") continue;
|
|
1063
|
+
const splits = splitBackgroundImage(val);
|
|
1064
|
+
const inlined = await Promise.all(
|
|
1065
|
+
splits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
1066
|
+
);
|
|
1067
|
+
if (inlined.some((p) => p && p !== "none" && !/^url\(undefined/.test(p))) {
|
|
1068
|
+
cloneNode.style.setProperty(prop, inlined.join(", "));
|
|
1038
1069
|
}
|
|
1039
|
-
continue;
|
|
1040
|
-
}
|
|
1041
|
-
const bgSplits = splitBackgroundImage(bg);
|
|
1042
|
-
const newBgParts = await Promise.all(
|
|
1043
|
-
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
1044
|
-
);
|
|
1045
|
-
if (newBgParts.some((p) => p && p !== "none" && !/^url\(undefined\)/.test(p))) {
|
|
1046
|
-
cloneNode.style.backgroundImage = newBgParts.join(", ");
|
|
1047
1070
|
}
|
|
1071
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
1048
1072
|
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
1049
1073
|
cloneNode.style.backgroundColor = bgColor;
|
|
1050
1074
|
}
|
|
@@ -1059,13 +1083,14 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
1059
1083
|
// src/core/capture.js
|
|
1060
1084
|
async function captureDOM(element, options = {}) {
|
|
1061
1085
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1086
|
+
cache.reset();
|
|
1062
1087
|
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
1063
|
-
let clone, classCSS
|
|
1088
|
+
let clone, classCSS;
|
|
1064
1089
|
let fontsCSS = "";
|
|
1065
1090
|
let baseCSS = "";
|
|
1066
1091
|
let dataURL;
|
|
1067
1092
|
let svgString;
|
|
1068
|
-
({ clone, classCSS
|
|
1093
|
+
({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));
|
|
1069
1094
|
await new Promise((resolve) => {
|
|
1070
1095
|
idle(async () => {
|
|
1071
1096
|
await inlineImages(clone, options);
|
|
@@ -1074,7 +1099,7 @@ async function captureDOM(element, options = {}) {
|
|
|
1074
1099
|
});
|
|
1075
1100
|
await new Promise((resolve) => {
|
|
1076
1101
|
idle(async () => {
|
|
1077
|
-
await inlineBackgroundImages(element, clone,
|
|
1102
|
+
await inlineBackgroundImages(element, clone, options);
|
|
1078
1103
|
resolve();
|
|
1079
1104
|
}, { fast });
|
|
1080
1105
|
});
|
|
@@ -1089,13 +1114,13 @@ async function captureDOM(element, options = {}) {
|
|
|
1089
1114
|
if (compress) {
|
|
1090
1115
|
const usedTags = collectUsedTagNames(clone).sort();
|
|
1091
1116
|
const tagKey = usedTags.join(",");
|
|
1092
|
-
if (
|
|
1093
|
-
baseCSS =
|
|
1117
|
+
if (cache.baseStyle.has(tagKey)) {
|
|
1118
|
+
baseCSS = cache.baseStyle.get(tagKey);
|
|
1094
1119
|
} else {
|
|
1095
1120
|
await new Promise((resolve) => {
|
|
1096
1121
|
idle(() => {
|
|
1097
1122
|
baseCSS = generateDedupedBaseCSS(usedTags);
|
|
1098
|
-
|
|
1123
|
+
cache.baseStyle.set(tagKey, baseCSS);
|
|
1099
1124
|
resolve();
|
|
1100
1125
|
}, { fast });
|
|
1101
1126
|
});
|
|
@@ -1304,10 +1329,7 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1304
1329
|
async function preCache(root = document, options = {}) {
|
|
1305
1330
|
const { embedFonts = true, reset = false } = options;
|
|
1306
1331
|
if (reset) {
|
|
1307
|
-
|
|
1308
|
-
bgCache.clear();
|
|
1309
|
-
resourceCache.clear();
|
|
1310
|
-
baseCSSCache.clear();
|
|
1332
|
+
cache.reset();
|
|
1311
1333
|
return;
|
|
1312
1334
|
}
|
|
1313
1335
|
await document.fonts.ready;
|
|
@@ -1320,9 +1342,9 @@ async function preCache(root = document, options = {}) {
|
|
|
1320
1342
|
const promises = [];
|
|
1321
1343
|
for (const img of imgEls) {
|
|
1322
1344
|
const src = img.src;
|
|
1323
|
-
if (!
|
|
1345
|
+
if (!cache.image.has(src)) {
|
|
1324
1346
|
promises.push(
|
|
1325
|
-
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) =>
|
|
1347
|
+
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => cache.image.set(src, dataURL)).catch(() => {
|
|
1326
1348
|
})
|
|
1327
1349
|
);
|
|
1328
1350
|
}
|