@zumer/snapdom 1.3.0 → 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 +125 -11
- package/dist/snapdom.js +834 -476
- package/dist/snapdom.min.js +15 -3
- package/dist/snapdom.min.mjs +15 -3
- package/dist/snapdom.mjs +352 -95
- package/package.json +3 -2
- package/types/snapdom.d.ts +2 -0
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,11 +228,12 @@ 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
|
}
|
|
212
|
-
function fetchImage(src, timeout = 3e3) {
|
|
236
|
+
function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
213
237
|
if (imageCache.has(src)) {
|
|
214
238
|
return Promise.resolve(imageCache.get(src));
|
|
215
239
|
}
|
|
@@ -218,7 +242,7 @@ function fetchImage(src, timeout = 3e3) {
|
|
|
218
242
|
reject(new Error("Image load timed out"));
|
|
219
243
|
}, timeout);
|
|
220
244
|
const image = new Image();
|
|
221
|
-
image.crossOrigin =
|
|
245
|
+
image.crossOrigin = crossOrigin;
|
|
222
246
|
image.onload = async () => {
|
|
223
247
|
clearTimeout(timeoutId);
|
|
224
248
|
try {
|
|
@@ -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
|
}
|
|
378
|
-
|
|
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
|
+
}
|
|
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,29 +511,117 @@ 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("
|
|
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) {
|
|
432
|
-
console.warn("
|
|
528
|
+
console.warn("[snapdom] Failed to fetch CSS:", link.href);
|
|
433
529
|
}
|
|
434
530
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
531
|
+
for (const sheet of document.styleSheets) {
|
|
532
|
+
try {
|
|
533
|
+
if (!sheet.href || links.every((link) => link.href !== sheet.href)) {
|
|
534
|
+
for (const rule of sheet.cssRules) {
|
|
535
|
+
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
536
|
+
const src = rule.style.getPropertyValue("src");
|
|
537
|
+
if (!src) continue;
|
|
538
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
539
|
+
let inlinedSrc = src;
|
|
540
|
+
const matches = Array.from(src.matchAll(urlRegex));
|
|
541
|
+
for (const match of matches) {
|
|
542
|
+
let rawUrl = match[2].trim();
|
|
543
|
+
if (!rawUrl) continue;
|
|
544
|
+
let url = rawUrl;
|
|
545
|
+
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
546
|
+
url = new URL(url, sheet.href || location.href).href;
|
|
547
|
+
}
|
|
548
|
+
if (ignoreIconFonts && isIconFont(url)) continue;
|
|
549
|
+
if (resourceCache.has(url)) {
|
|
550
|
+
processedFontURLs.add(url);
|
|
551
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (processedFontURLs.has(url)) continue;
|
|
555
|
+
try {
|
|
556
|
+
const res = await fetch(url);
|
|
557
|
+
const blob = await res.blob();
|
|
558
|
+
const b64 = await new Promise((resolve) => {
|
|
559
|
+
const reader = new FileReader();
|
|
560
|
+
reader.onload = () => resolve(reader.result);
|
|
561
|
+
reader.readAsDataURL(blob);
|
|
562
|
+
});
|
|
563
|
+
resourceCache.set(url, b64);
|
|
564
|
+
processedFontURLs.add(url);
|
|
565
|
+
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
finalCSS += `@font-face {
|
|
571
|
+
font-family: ${rule.style.getPropertyValue("font-family")};
|
|
572
|
+
src: ${inlinedSrc};
|
|
573
|
+
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
574
|
+
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
575
|
+
}
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (e) {
|
|
581
|
+
console.warn("[snapdom] Cannot access stylesheet", sheet.href, e);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
for (const font of document.fonts) {
|
|
585
|
+
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
586
|
+
let b64 = font._snapdomSrc;
|
|
587
|
+
if (!b64.startsWith("data:")) {
|
|
588
|
+
if (resourceCache.has(font._snapdomSrc)) {
|
|
589
|
+
b64 = resourceCache.get(font._snapdomSrc);
|
|
590
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
591
|
+
} else if (!processedFontURLs.has(font._snapdomSrc)) {
|
|
592
|
+
try {
|
|
593
|
+
const res = await fetch(font._snapdomSrc);
|
|
594
|
+
const blob = await res.blob();
|
|
595
|
+
b64 = await new Promise((resolve) => {
|
|
596
|
+
const reader = new FileReader();
|
|
597
|
+
reader.onload = () => resolve(reader.result);
|
|
598
|
+
reader.readAsDataURL(blob);
|
|
599
|
+
});
|
|
600
|
+
resourceCache.set(font._snapdomSrc, b64);
|
|
601
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
finalCSS += `@font-face {
|
|
609
|
+
font-family: '${font.family}';
|
|
610
|
+
src: url(${b64});
|
|
611
|
+
font-style: ${font.style || "normal"};
|
|
612
|
+
font-weight: ${font.weight || "normal"};
|
|
613
|
+
}
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
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
|
+
}
|
|
440
625
|
}
|
|
441
626
|
return finalCSS;
|
|
442
627
|
}
|
|
@@ -444,15 +629,41 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
|
444
629
|
// src/modules/pseudo.js
|
|
445
630
|
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
|
|
446
631
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
447
|
-
for (const pseudo of ["::before", "::after"]) {
|
|
632
|
+
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
448
633
|
try {
|
|
449
634
|
const style = getStyle(source, pseudo);
|
|
450
|
-
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
|
+
}
|
|
451
660
|
const content = style.getPropertyValue("content");
|
|
452
661
|
const bg = style.getPropertyValue("background-image");
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
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) {
|
|
456
667
|
const fontFamily = style.getPropertyValue("font-family");
|
|
457
668
|
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
|
|
458
669
|
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
|
|
@@ -462,27 +673,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
462
673
|
const snapshot = snapshotComputedStyle(style);
|
|
463
674
|
const key = getStyleKey(snapshot, "span", compress);
|
|
464
675
|
styleMap.set(pseudoEl, key);
|
|
465
|
-
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(
|
|
466
677
|
fontFamily
|
|
467
678
|
);
|
|
468
|
-
|
|
679
|
+
const cleanContent = parseContent(content);
|
|
469
680
|
if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
|
|
470
681
|
const imgEl = document.createElement("img");
|
|
471
|
-
imgEl.src = await iconToImage(
|
|
472
|
-
cleanContent,
|
|
473
|
-
fontFamily,
|
|
474
|
-
fontWeight,
|
|
475
|
-
fontSize,
|
|
476
|
-
color
|
|
477
|
-
);
|
|
682
|
+
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
478
683
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
479
684
|
pseudoEl.appendChild(imgEl);
|
|
480
685
|
} else if (cleanContent.startsWith("url(")) {
|
|
481
686
|
const rawUrl = extractURL(cleanContent);
|
|
482
|
-
if (rawUrl) {
|
|
687
|
+
if (rawUrl && rawUrl.trim() !== "") {
|
|
483
688
|
try {
|
|
484
689
|
const imgEl = document.createElement("img");
|
|
485
|
-
const dataUrl = await fetchImage(
|
|
690
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
|
|
486
691
|
imgEl.src = dataUrl;
|
|
487
692
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
488
693
|
pseudoEl.appendChild(imgEl);
|
|
@@ -493,24 +698,22 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
493
698
|
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
494
699
|
pseudoEl.textContent = cleanContent;
|
|
495
700
|
}
|
|
496
|
-
if (hasBg
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
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);
|
|
505
710
|
}
|
|
506
711
|
}
|
|
712
|
+
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
507
713
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
} else {
|
|
512
|
-
clone.appendChild(pseudoEl);
|
|
513
|
-
}
|
|
714
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor;
|
|
715
|
+
if (!hasVisibleBox) continue;
|
|
716
|
+
pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
|
|
514
717
|
}
|
|
515
718
|
} catch (e) {
|
|
516
719
|
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
@@ -584,16 +787,32 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
584
787
|
cloneNode.appendChild(inner);
|
|
585
788
|
}
|
|
586
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
|
+
}
|
|
587
805
|
return { clone, classCSS, styleCache };
|
|
588
806
|
}
|
|
589
807
|
|
|
590
808
|
// src/modules/images.js
|
|
591
|
-
async function inlineImages(clone) {
|
|
809
|
+
async function inlineImages(clone, options = {}) {
|
|
592
810
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
593
811
|
const processImg = async (img) => {
|
|
594
812
|
const src = img.src;
|
|
595
813
|
try {
|
|
596
|
-
const
|
|
814
|
+
const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
|
|
815
|
+
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
597
816
|
img.src = dataUrl;
|
|
598
817
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
599
818
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -611,28 +830,34 @@ async function inlineImages(clone) {
|
|
|
611
830
|
}
|
|
612
831
|
|
|
613
832
|
// src/modules/background.js
|
|
614
|
-
async function inlineBackgroundImages(source, clone, styleCache) {
|
|
833
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
615
834
|
const queue = [[source, clone]];
|
|
616
835
|
while (queue.length) {
|
|
617
836
|
const [srcNode, cloneNode] = queue.shift();
|
|
618
837
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
619
838
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
620
839
|
const bg = style.getPropertyValue("background-image");
|
|
621
|
-
const
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
dataUrl = bgCache.get(bgUrl);
|
|
628
|
-
} else {
|
|
629
|
-
dataUrl = await fetchImage(bgUrl);
|
|
630
|
-
bgCache.set(bgUrl, dataUrl);
|
|
631
|
-
}
|
|
632
|
-
cloneNode.style.backgroundImage = `url(${dataUrl})`;
|
|
633
|
-
} catch {
|
|
634
|
-
cloneNode.style.backgroundImage = "none";
|
|
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]]);
|
|
635
846
|
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
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;
|
|
636
861
|
}
|
|
637
862
|
const sChildren = Array.from(srcNode.children);
|
|
638
863
|
const cChildren = Array.from(cloneNode.children);
|
|
@@ -645,6 +870,7 @@ async function inlineBackgroundImages(source, clone, styleCache) {
|
|
|
645
870
|
// src/core/capture.js
|
|
646
871
|
async function captureDOM(element, options = {}) {
|
|
647
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");
|
|
648
874
|
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
649
875
|
let clone, classCSS, styleCache;
|
|
650
876
|
let fontsCSS = "";
|
|
@@ -654,13 +880,13 @@ async function captureDOM(element, options = {}) {
|
|
|
654
880
|
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
655
881
|
await new Promise((resolve) => {
|
|
656
882
|
idle(async () => {
|
|
657
|
-
await inlineImages(clone);
|
|
883
|
+
await inlineImages(clone, options);
|
|
658
884
|
resolve();
|
|
659
885
|
}, { fast });
|
|
660
886
|
});
|
|
661
887
|
await new Promise((resolve) => {
|
|
662
888
|
idle(async () => {
|
|
663
|
-
await inlineBackgroundImages(element, clone, styleCache);
|
|
889
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
664
890
|
resolve();
|
|
665
891
|
}, { fast });
|
|
666
892
|
});
|
|
@@ -690,11 +916,37 @@ async function captureDOM(element, options = {}) {
|
|
|
690
916
|
await new Promise((resolve) => {
|
|
691
917
|
idle(() => {
|
|
692
918
|
const rect = element.getBoundingClientRect();
|
|
693
|
-
|
|
694
|
-
|
|
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);
|
|
695
939
|
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
696
940
|
clone.style.transformOrigin = "top left";
|
|
697
|
-
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()) {
|
|
698
950
|
clone.style.scale = `${scale}`;
|
|
699
951
|
}
|
|
700
952
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
@@ -842,13 +1094,12 @@ snapdom.download = async (el, options = {}) => {
|
|
|
842
1094
|
|
|
843
1095
|
// src/api/preCache.js
|
|
844
1096
|
async function preCache(root = document, options = {}) {
|
|
845
|
-
const { embedFonts = true, reset = false,
|
|
1097
|
+
const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
|
|
846
1098
|
if (reset) {
|
|
847
1099
|
imageCache.clear();
|
|
848
1100
|
bgCache.clear();
|
|
849
1101
|
resourceCache.clear();
|
|
850
1102
|
baseCSSCache.clear();
|
|
851
|
-
computedStyleCache.clear();
|
|
852
1103
|
return;
|
|
853
1104
|
}
|
|
854
1105
|
await document.fonts.ready;
|
|
@@ -862,20 +1113,26 @@ async function preCache(root = document, options = {}) {
|
|
|
862
1113
|
for (const img of imgEls) {
|
|
863
1114
|
const src = img.src;
|
|
864
1115
|
if (!imageCache.has(src)) {
|
|
1116
|
+
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
865
1117
|
promises.push(
|
|
866
|
-
fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
1118
|
+
fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
867
1119
|
})
|
|
868
1120
|
);
|
|
869
1121
|
}
|
|
870
1122
|
}
|
|
871
1123
|
for (const el of allEls) {
|
|
872
|
-
const bg =
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
}
|
|
879
1136
|
}
|
|
880
1137
|
}
|
|
881
1138
|
if (embedFonts) {
|