@zumer/snapdom 1.7.1 → 1.8.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 +92 -5
- package/dist/snapdom.js +814 -549
- package/dist/snapdom.min.js +13 -13
- package/dist/snapdom.min.mjs +13 -13
- package/dist/snapdom.mjs +261 -97
- package/package.json +3 -2
package/dist/snapdom.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.
|
|
4
|
+
* v.1.8.0
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
// src/core/cache.js
|
|
10
11
|
var imageCache = /* @__PURE__ */ new Map();
|
|
11
12
|
var bgCache = /* @__PURE__ */ new Map();
|
|
@@ -13,6 +14,7 @@ var resourceCache = /* @__PURE__ */ new Map();
|
|
|
13
14
|
var defaultStylesCache = /* @__PURE__ */ new Map();
|
|
14
15
|
var baseCSSCache = /* @__PURE__ */ new Map();
|
|
15
16
|
var computedStyleCache = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
var processedFontURLs = /* @__PURE__ */ new Set();
|
|
16
18
|
|
|
17
19
|
// src/utils/cssTools.js
|
|
18
20
|
var commonTags = [
|
|
@@ -152,6 +154,27 @@ function generateCSSClasses(styleMap) {
|
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
// src/utils/helpers.js
|
|
157
|
+
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
158
|
+
const isUrl = entry.startsWith("url(");
|
|
159
|
+
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
160
|
+
if (isUrl) {
|
|
161
|
+
const rawUrl = extractURL(entry);
|
|
162
|
+
if (!rawUrl) return entry;
|
|
163
|
+
const encodedUrl = safeEncodeURI(rawUrl);
|
|
164
|
+
if (bgCache.has(encodedUrl)) {
|
|
165
|
+
return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
|
|
166
|
+
} else {
|
|
167
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(encodedUrl) : "anonymous";
|
|
168
|
+
const dataUrl = await fetchImage(encodedUrl, 3e3, crossOrigin);
|
|
169
|
+
bgCache.set(encodedUrl, dataUrl);
|
|
170
|
+
return options.skipInline ? void 0 : `url(${dataUrl})`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (isGradient || entry === "none") {
|
|
174
|
+
return entry;
|
|
175
|
+
}
|
|
176
|
+
return entry;
|
|
177
|
+
}
|
|
155
178
|
function idle(fn, { fast = false } = {}) {
|
|
156
179
|
if (fast) return fn();
|
|
157
180
|
if ("requestIdleCallback" in window) {
|
|
@@ -205,7 +228,8 @@ function isIconFont(familyOrUrl) {
|
|
|
205
228
|
/feather/i,
|
|
206
229
|
/bootstrap\s*icons/i,
|
|
207
230
|
/remix\s*icons/i,
|
|
208
|
-
/heroicons/i
|
|
231
|
+
/heroicons/i,
|
|
232
|
+
/lucide/i
|
|
209
233
|
];
|
|
210
234
|
return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
|
|
211
235
|
}
|
|
@@ -256,6 +280,49 @@ function snapshotComputedStyle(style) {
|
|
|
256
280
|
function isSafari() {
|
|
257
281
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
258
282
|
}
|
|
283
|
+
function stripTranslate(transform) {
|
|
284
|
+
if (!transform || transform === "none") return "";
|
|
285
|
+
let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
|
|
286
|
+
cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
|
|
287
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
288
|
+
if (parts.length !== 6) return `matrix(${values})`;
|
|
289
|
+
parts[4] = "0";
|
|
290
|
+
parts[5] = "0";
|
|
291
|
+
return `matrix(${parts.join(", ")})`;
|
|
292
|
+
});
|
|
293
|
+
cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
|
|
294
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
295
|
+
if (parts.length !== 16) return `matrix3d(${values})`;
|
|
296
|
+
parts[12] = "0";
|
|
297
|
+
parts[13] = "0";
|
|
298
|
+
return `matrix3d(${parts.join(", ")})`;
|
|
299
|
+
});
|
|
300
|
+
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
301
|
+
}
|
|
302
|
+
function safeEncodeURI(uri) {
|
|
303
|
+
try {
|
|
304
|
+
const newURI = decodeURI(uri);
|
|
305
|
+
return encodeURI(newURI);
|
|
306
|
+
} catch {
|
|
307
|
+
return uri;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function splitBackgroundImage(bg) {
|
|
311
|
+
const parts = [];
|
|
312
|
+
let depth = 0;
|
|
313
|
+
let lastIndex = 0;
|
|
314
|
+
for (let i = 0; i < bg.length; i++) {
|
|
315
|
+
const char = bg[i];
|
|
316
|
+
if (char === "(") depth++;
|
|
317
|
+
if (char === ")") depth--;
|
|
318
|
+
if (char === "," && depth === 0) {
|
|
319
|
+
parts.push(bg.slice(lastIndex, i).trim());
|
|
320
|
+
lastIndex = i + 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
parts.push(bg.slice(lastIndex).trim());
|
|
324
|
+
return parts;
|
|
325
|
+
}
|
|
259
326
|
|
|
260
327
|
// src/modules/styles.js
|
|
261
328
|
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
@@ -281,7 +348,7 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
281
348
|
if (node.tagName === "IFRAME") {
|
|
282
349
|
const fallback = document.createElement("div");
|
|
283
350
|
fallback.textContent = "";
|
|
284
|
-
fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`;
|
|
351
|
+
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;`;
|
|
285
352
|
return fallback;
|
|
286
353
|
}
|
|
287
354
|
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
|
|
@@ -301,8 +368,8 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
301
368
|
img.width = node.width;
|
|
302
369
|
img.height = node.height;
|
|
303
370
|
img.style.display = "inline-block";
|
|
304
|
-
img.style.width = `${node.width}px`;
|
|
305
|
-
img.style.height = `${node.height}px`;
|
|
371
|
+
img.style.width = node.style.width || `${node.width}px`;
|
|
372
|
+
img.style.height = node.style.height || `${node.height}px`;
|
|
306
373
|
return img;
|
|
307
374
|
}
|
|
308
375
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -375,36 +442,66 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
375
442
|
ctx.fillText(unicodeChar, 0, ascent);
|
|
376
443
|
return canvas.toDataURL();
|
|
377
444
|
}
|
|
445
|
+
function isStylesheetLoaded(href) {
|
|
446
|
+
return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
|
|
447
|
+
}
|
|
448
|
+
function injectLinkIfMissing(href) {
|
|
449
|
+
return new Promise((resolve) => {
|
|
450
|
+
if (isStylesheetLoaded(href)) return resolve(null);
|
|
451
|
+
const link = document.createElement("link");
|
|
452
|
+
link.rel = "stylesheet";
|
|
453
|
+
link.href = href;
|
|
454
|
+
link.setAttribute("data-snapdom", "injected-import");
|
|
455
|
+
link.onload = () => resolve(link);
|
|
456
|
+
link.onerror = () => resolve(null);
|
|
457
|
+
document.head.appendChild(link);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
378
460
|
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
|
|
461
|
+
if (resourceCache.has("fonts-embed-css")) {
|
|
462
|
+
if (preCached) {
|
|
463
|
+
const style = document.createElement("style");
|
|
464
|
+
style.setAttribute("data-snapdom", "embedFonts");
|
|
465
|
+
style.textContent = resourceCache.get("fonts-embed-css");
|
|
466
|
+
document.head.appendChild(style);
|
|
467
|
+
}
|
|
468
|
+
return resourceCache.get("fonts-embed-css");
|
|
469
|
+
}
|
|
470
|
+
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
471
|
+
const styleImports = [];
|
|
472
|
+
for (const styleTag of document.querySelectorAll("style")) {
|
|
473
|
+
const cssText = styleTag.textContent || "";
|
|
474
|
+
const matches = Array.from(cssText.matchAll(importRegex));
|
|
475
|
+
for (const match of matches) {
|
|
476
|
+
const importUrl = match[1];
|
|
477
|
+
if (!isStylesheetLoaded(importUrl)) {
|
|
478
|
+
styleImports.push(importUrl);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
await Promise.all(styleImports.map(injectLinkIfMissing));
|
|
379
483
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
|
|
380
484
|
let finalCSS = "";
|
|
381
485
|
for (const link of links) {
|
|
382
486
|
try {
|
|
383
487
|
const res = await fetch(link.href);
|
|
384
488
|
const cssText = await res.text();
|
|
385
|
-
if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText)))
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
const urlRegex = /url\(([^)]+)\)/g;
|
|
489
|
+
if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) continue;
|
|
490
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
389
491
|
const inlinedCSS = await Promise.all(
|
|
390
492
|
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
|
|
391
493
|
let rawUrl = extractURL(match[0]);
|
|
392
494
|
if (!rawUrl) return null;
|
|
393
495
|
let url = rawUrl;
|
|
394
|
-
if (!url.startsWith("http")) {
|
|
496
|
+
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
395
497
|
url = new URL(url, link.href).href;
|
|
396
498
|
}
|
|
397
|
-
|
|
398
|
-
url = encodeURI(url);
|
|
399
|
-
} catch (e) {
|
|
400
|
-
console.warn("[snapdom] Failed to encode font URL:", url);
|
|
401
|
-
}
|
|
402
|
-
if (ignoreIconFonts && isIconFont(url)) {
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
499
|
+
if (ignoreIconFonts && isIconFont(url)) return null;
|
|
405
500
|
if (resourceCache.has(url)) {
|
|
501
|
+
processedFontURLs.add(url);
|
|
406
502
|
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
|
|
407
503
|
}
|
|
504
|
+
if (processedFontURLs.has(url)) return null;
|
|
408
505
|
try {
|
|
409
506
|
const fontRes = await fetch(url);
|
|
410
507
|
const blob = await fontRes.blob();
|
|
@@ -414,18 +511,17 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
414
511
|
reader.readAsDataURL(blob);
|
|
415
512
|
});
|
|
416
513
|
resourceCache.set(url, b64);
|
|
514
|
+
processedFontURLs.add(url);
|
|
417
515
|
return { original: match[0], inlined: `url(${b64})` };
|
|
418
|
-
} catch (
|
|
419
|
-
console.warn("[snapdom] Failed to fetch font:", url);
|
|
516
|
+
} catch (e) {
|
|
517
|
+
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
420
518
|
return null;
|
|
421
519
|
}
|
|
422
520
|
})
|
|
423
521
|
);
|
|
424
522
|
let cssFinal = cssText;
|
|
425
523
|
for (const r of inlinedCSS) {
|
|
426
|
-
if (r)
|
|
427
|
-
cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
428
|
-
}
|
|
524
|
+
if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
429
525
|
}
|
|
430
526
|
finalCSS += cssFinal + "\n";
|
|
431
527
|
} catch (e) {
|
|
@@ -439,23 +535,23 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
439
535
|
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
440
536
|
const src = rule.style.getPropertyValue("src");
|
|
441
537
|
if (!src) continue;
|
|
442
|
-
const urlRegex = /url\(([^)]+)\)/g;
|
|
538
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
443
539
|
let inlinedSrc = src;
|
|
444
540
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
445
541
|
for (const match of matches) {
|
|
446
|
-
let rawUrl = match[
|
|
542
|
+
let rawUrl = match[2].trim();
|
|
447
543
|
if (!rawUrl) continue;
|
|
448
544
|
let url = rawUrl;
|
|
449
545
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
450
546
|
url = new URL(url, sheet.href || location.href).href;
|
|
451
547
|
}
|
|
452
|
-
if (ignoreIconFonts && isIconFont(url))
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
548
|
+
if (ignoreIconFonts && isIconFont(url)) continue;
|
|
455
549
|
if (resourceCache.has(url)) {
|
|
550
|
+
processedFontURLs.add(url);
|
|
456
551
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
457
552
|
continue;
|
|
458
553
|
}
|
|
554
|
+
if (processedFontURLs.has(url)) continue;
|
|
459
555
|
try {
|
|
460
556
|
const res = await fetch(url);
|
|
461
557
|
const blob = await res.blob();
|
|
@@ -465,6 +561,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
465
561
|
reader.readAsDataURL(blob);
|
|
466
562
|
});
|
|
467
563
|
resourceCache.set(url, b64);
|
|
564
|
+
processedFontURLs.add(url);
|
|
468
565
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
469
566
|
} catch (e) {
|
|
470
567
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
@@ -490,7 +587,8 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
490
587
|
if (!b64.startsWith("data:")) {
|
|
491
588
|
if (resourceCache.has(font._snapdomSrc)) {
|
|
492
589
|
b64 = resourceCache.get(font._snapdomSrc);
|
|
493
|
-
|
|
590
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
591
|
+
} else if (!processedFontURLs.has(font._snapdomSrc)) {
|
|
494
592
|
try {
|
|
495
593
|
const res = await fetch(font._snapdomSrc);
|
|
496
594
|
const blob = await res.blob();
|
|
@@ -500,6 +598,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
500
598
|
reader.readAsDataURL(blob);
|
|
501
599
|
});
|
|
502
600
|
resourceCache.set(font._snapdomSrc, b64);
|
|
601
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
503
602
|
} catch (e) {
|
|
504
603
|
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
505
604
|
continue;
|
|
@@ -515,11 +614,14 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
515
614
|
`;
|
|
516
615
|
}
|
|
517
616
|
}
|
|
518
|
-
if (finalCSS
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
617
|
+
if (finalCSS) {
|
|
618
|
+
resourceCache.set("fonts-embed-css", finalCSS);
|
|
619
|
+
if (preCached) {
|
|
620
|
+
const style = document.createElement("style");
|
|
621
|
+
style.setAttribute("data-snapdom", "embedFonts");
|
|
622
|
+
style.textContent = finalCSS;
|
|
623
|
+
document.head.appendChild(style);
|
|
624
|
+
}
|
|
523
625
|
}
|
|
524
626
|
return finalCSS;
|
|
525
627
|
}
|
|
@@ -527,15 +629,41 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
527
629
|
// src/modules/pseudo.js
|
|
528
630
|
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
|
|
529
631
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
530
|
-
for (const pseudo of ["::before", "::after"]) {
|
|
632
|
+
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
531
633
|
try {
|
|
532
634
|
const style = getStyle(source, pseudo);
|
|
533
|
-
if (!style) continue;
|
|
635
|
+
if (!style || typeof style[Symbol.iterator] !== "function") continue;
|
|
636
|
+
if (pseudo === "::first-letter") {
|
|
637
|
+
const normal = getComputedStyle(source);
|
|
638
|
+
const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
|
|
639
|
+
if (!isMeaningful) continue;
|
|
640
|
+
const textNode = Array.from(clone.childNodes).find(
|
|
641
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
|
|
642
|
+
);
|
|
643
|
+
if (!textNode) continue;
|
|
644
|
+
const text = textNode.textContent;
|
|
645
|
+
const match = text.match(/^([^\p{L}\p{N}\s]*[\p{L}\p{N}](?:['’])?)/u);
|
|
646
|
+
const first = match?.[0];
|
|
647
|
+
const rest = text.slice(first?.length || 0);
|
|
648
|
+
if (!first || /[\uD800-\uDFFF]/.test(first)) continue;
|
|
649
|
+
const span = document.createElement("span");
|
|
650
|
+
span.textContent = first;
|
|
651
|
+
span.dataset.snapdomPseudo = "::first-letter";
|
|
652
|
+
const snapshot = snapshotComputedStyle(style);
|
|
653
|
+
const key = getStyleKey(snapshot, "span", compress);
|
|
654
|
+
styleMap.set(span, key);
|
|
655
|
+
const restNode = document.createTextNode(rest);
|
|
656
|
+
clone.replaceChild(restNode, textNode);
|
|
657
|
+
clone.insertBefore(span, restNode);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
534
660
|
const content = style.getPropertyValue("content");
|
|
535
661
|
const bg = style.getPropertyValue("background-image");
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
662
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
663
|
+
const hasContent = content !== "none";
|
|
664
|
+
const hasBg = bg && bg !== "none";
|
|
665
|
+
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
666
|
+
if (hasContent || hasBg || hasBgColor) {
|
|
539
667
|
const fontFamily = style.getPropertyValue("font-family");
|
|
540
668
|
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
|
|
541
669
|
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
|
|
@@ -545,27 +673,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
545
673
|
const snapshot = snapshotComputedStyle(style);
|
|
546
674
|
const key = getStyleKey(snapshot, "span", compress);
|
|
547
675
|
styleMap.set(pseudoEl, key);
|
|
548
|
-
const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(
|
|
676
|
+
const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons|lucide/i.test(
|
|
549
677
|
fontFamily
|
|
550
678
|
);
|
|
551
|
-
|
|
679
|
+
const cleanContent = parseContent(content);
|
|
552
680
|
if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
|
|
553
681
|
const imgEl = document.createElement("img");
|
|
554
|
-
imgEl.src = await iconToImage(
|
|
555
|
-
cleanContent,
|
|
556
|
-
fontFamily,
|
|
557
|
-
fontWeight,
|
|
558
|
-
fontSize,
|
|
559
|
-
color
|
|
560
|
-
);
|
|
682
|
+
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
561
683
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
562
684
|
pseudoEl.appendChild(imgEl);
|
|
563
685
|
} else if (cleanContent.startsWith("url(")) {
|
|
564
686
|
const rawUrl = extractURL(cleanContent);
|
|
565
|
-
if (rawUrl) {
|
|
687
|
+
if (rawUrl && rawUrl.trim() !== "") {
|
|
566
688
|
try {
|
|
567
689
|
const imgEl = document.createElement("img");
|
|
568
|
-
const dataUrl = await fetchImage(
|
|
690
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
|
|
569
691
|
imgEl.src = dataUrl;
|
|
570
692
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
571
693
|
pseudoEl.appendChild(imgEl);
|
|
@@ -576,24 +698,22 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
576
698
|
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
577
699
|
pseudoEl.textContent = cleanContent;
|
|
578
700
|
}
|
|
579
|
-
if (hasBg
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
701
|
+
if (hasBg) {
|
|
702
|
+
try {
|
|
703
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
704
|
+
const newBgParts = await Promise.all(
|
|
705
|
+
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
|
|
706
|
+
);
|
|
707
|
+
pseudoEl.style.backgroundImage = newBgParts.join(", ");
|
|
708
|
+
} catch (e) {
|
|
709
|
+
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
588
710
|
}
|
|
589
711
|
}
|
|
712
|
+
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
590
713
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
|
|
591
|
-
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
} else {
|
|
595
|
-
clone.appendChild(pseudoEl);
|
|
596
|
-
}
|
|
714
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor;
|
|
715
|
+
if (!hasVisibleBox) continue;
|
|
716
|
+
pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
|
|
597
717
|
}
|
|
598
718
|
} catch (e) {
|
|
599
719
|
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
@@ -667,6 +787,21 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
667
787
|
cloneNode.appendChild(inner);
|
|
668
788
|
}
|
|
669
789
|
}
|
|
790
|
+
if (element === nodeMap.get(clone)) {
|
|
791
|
+
const computed = styleCache.get(element) || window.getComputedStyle(element);
|
|
792
|
+
styleCache.set(element, computed);
|
|
793
|
+
const transform = stripTranslate(computed.transform);
|
|
794
|
+
clone.style.margin = "0";
|
|
795
|
+
clone.style.position = "static";
|
|
796
|
+
clone.style.top = "auto";
|
|
797
|
+
clone.style.left = "auto";
|
|
798
|
+
clone.style.right = "auto";
|
|
799
|
+
clone.style.bottom = "auto";
|
|
800
|
+
clone.style.zIndex = "auto";
|
|
801
|
+
clone.style.float = "none";
|
|
802
|
+
clone.style.clear = "none";
|
|
803
|
+
clone.style.transform = transform || "";
|
|
804
|
+
}
|
|
670
805
|
return { clone, classCSS, styleCache };
|
|
671
806
|
}
|
|
672
807
|
|
|
@@ -702,28 +837,27 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
702
837
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
703
838
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
704
839
|
const bg = style.getPropertyValue("background-image");
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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";
|
|
722
|
-
}
|
|
840
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
841
|
+
if (!bg || bg === "none") {
|
|
842
|
+
const sChildren2 = Array.from(srcNode.children);
|
|
843
|
+
const cChildren2 = Array.from(cloneNode.children);
|
|
844
|
+
for (let i = 0; i < Math.min(sChildren2.length, cChildren2.length); i++) {
|
|
845
|
+
queue.push([sChildren2[i], cChildren2[i]]);
|
|
723
846
|
}
|
|
847
|
+
continue;
|
|
724
848
|
}
|
|
725
|
-
|
|
726
|
-
|
|
849
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
850
|
+
const newBgParts = await Promise.all(
|
|
851
|
+
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
852
|
+
);
|
|
853
|
+
const hasRealBg = newBgParts.some(
|
|
854
|
+
(p) => p && p !== "none" && !/^url\(undefined\)/.test(p)
|
|
855
|
+
);
|
|
856
|
+
if (hasRealBg) {
|
|
857
|
+
cloneNode.style.backgroundImage = newBgParts.join(", ");
|
|
858
|
+
}
|
|
859
|
+
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
860
|
+
cloneNode.style.backgroundColor = bgColor;
|
|
727
861
|
}
|
|
728
862
|
const sChildren = Array.from(srcNode.children);
|
|
729
863
|
const cChildren = Array.from(cloneNode.children);
|
|
@@ -736,6 +870,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
736
870
|
// src/core/capture.js
|
|
737
871
|
async function captureDOM(element, options = {}) {
|
|
738
872
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
873
|
+
if (!(element instanceof Element)) throw new Error("captureDOM: Only Element nodes are supported");
|
|
739
874
|
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
740
875
|
let clone, classCSS, styleCache;
|
|
741
876
|
let fontsCSS = "";
|
|
@@ -781,11 +916,37 @@ async function captureDOM(element, options = {}) {
|
|
|
781
916
|
await new Promise((resolve) => {
|
|
782
917
|
idle(() => {
|
|
783
918
|
const rect = element.getBoundingClientRect();
|
|
784
|
-
|
|
785
|
-
|
|
919
|
+
let w = rect.width;
|
|
920
|
+
let h = rect.height;
|
|
921
|
+
const hasW = Number.isFinite(options.width);
|
|
922
|
+
const hasH = Number.isFinite(options.height);
|
|
923
|
+
const hasScale = typeof scale === "number" && scale !== 1;
|
|
924
|
+
if (!hasScale) {
|
|
925
|
+
const aspect = rect.width / rect.height;
|
|
926
|
+
if (hasW && hasH) {
|
|
927
|
+
w = options.width;
|
|
928
|
+
h = options.height;
|
|
929
|
+
} else if (hasW) {
|
|
930
|
+
w = options.width;
|
|
931
|
+
h = w / aspect;
|
|
932
|
+
} else if (hasH) {
|
|
933
|
+
h = options.height;
|
|
934
|
+
w = h * aspect;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
w = Math.ceil(w);
|
|
938
|
+
h = Math.ceil(h);
|
|
786
939
|
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
787
940
|
clone.style.transformOrigin = "top left";
|
|
788
|
-
if (
|
|
941
|
+
if (!hasScale && (hasW || hasH)) {
|
|
942
|
+
const originalW = rect.width;
|
|
943
|
+
const originalH = rect.height;
|
|
944
|
+
const scaleX = w / originalW;
|
|
945
|
+
const scaleY = h / originalH;
|
|
946
|
+
const existingTransform = clone.style.transform || "";
|
|
947
|
+
const scaleTransform = `scale(${scaleX}, ${scaleY})`;
|
|
948
|
+
clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
|
|
949
|
+
} else if (hasScale && isSafari()) {
|
|
789
950
|
clone.style.scale = `${scale}`;
|
|
790
951
|
}
|
|
791
952
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
@@ -939,7 +1100,6 @@ async function preCache(root = document, options = {}) {
|
|
|
939
1100
|
bgCache.clear();
|
|
940
1101
|
resourceCache.clear();
|
|
941
1102
|
baseCSSCache.clear();
|
|
942
|
-
computedStyleCache.clear();
|
|
943
1103
|
return;
|
|
944
1104
|
}
|
|
945
1105
|
await document.fonts.ready;
|
|
@@ -961,14 +1121,18 @@ async function preCache(root = document, options = {}) {
|
|
|
961
1121
|
}
|
|
962
1122
|
}
|
|
963
1123
|
for (const el of allEls) {
|
|
964
|
-
const bg =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1124
|
+
const bg = getStyle(el).backgroundImage;
|
|
1125
|
+
if (bg && bg !== "none") {
|
|
1126
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
1127
|
+
for (const entry of bgSplits) {
|
|
1128
|
+
const isUrl = entry.startsWith("url(");
|
|
1129
|
+
if (isUrl) {
|
|
1130
|
+
promises.push(
|
|
1131
|
+
inlineSingleBackgroundEntry(entry, { crossOrigin: crossOriginFn, skipInline: true }).catch(() => {
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
972
1136
|
}
|
|
973
1137
|
}
|
|
974
1138
|
if (embedFonts) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zumer/snapdom",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
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": {
|
|
8
8
|
"test": "npx vitest run --browser.headless --reporter=verbose",
|
|
9
9
|
"test:benchmark": "npx vitest bench --browser.headless --watch=false",
|
|
10
|
-
"compile-browser": "npx @zumerbox/build -n snapdom -j src/index.browser.js",
|
|
10
|
+
"compile-browser": "npx @zumerbox/build -n snapdom -j src/index.browser.js -t es2015",
|
|
11
11
|
"compile-esm": "npx @zumerbox/build -n snapdom -j src/index.js -p neutral -x",
|
|
12
12
|
"compile": "npm run compile-browser && npm run compile-esm",
|
|
13
13
|
"build": "npm run compile && npm pack",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"homepage": "https://zumerlab.github.io/snapdom/",
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@vitest/browser": "^3.1.2",
|
|
69
|
+
"@vitest/coverage-v8": "^3.1.2",
|
|
69
70
|
"playwright": "^1.52.0"
|
|
70
71
|
}
|
|
71
72
|
}
|