@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/dist/snapdom.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.3.0
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 = "anonymous";
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
- async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
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
- continue;
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
- try {
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 (err) {
419
- console.warn("\u274C 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) {
432
- console.warn("\u274C Failed to fetch CSS:", link.href);
528
+ console.warn("[snapdom] Failed to fetch CSS:", link.href);
433
529
  }
434
530
  }
435
- if (finalCSS && preCached) {
436
- const style = document.createElement("style");
437
- style.setAttribute("data-snapdom", "embedFonts");
438
- style.textContent = finalCSS;
439
- document.head.appendChild(style);
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 hasContent = content && content !== "none" && content !== '""' && content !== "''";
454
- const hasBg = bg && bg.startsWith("url(");
455
- if (hasContent || hasBg) {
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
- let cleanContent = parseContent(content);
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(encodeURI(rawUrl));
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 && bg) {
497
- const rawUrl = extractURL(bg);
498
- if (rawUrl) {
499
- try {
500
- const dataUrl = await fetchImage(encodeURI(rawUrl));
501
- pseudoEl.style.backgroundImage = `url(${dataUrl})`;
502
- } catch (e) {
503
- console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
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
- if (!hasContent2 && !hasBg) continue;
509
- if (pseudo === "::before") {
510
- clone.insertBefore(pseudoEl, clone.firstChild);
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 dataUrl = await fetchImage(src);
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 rawUrl = extractURL(bg);
622
- if (rawUrl) {
623
- try {
624
- let bgUrl = encodeURI(rawUrl);
625
- let dataUrl;
626
- if (bgCache.has(bgUrl)) {
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
- const w = Math.ceil(rect.width);
694
- const h = Math.ceil(rect.height);
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 (scale !== 1 && isSafari()) {
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, preWarm = true } = options;
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 = getComputedStyle(el).backgroundImage;
873
- const url = extractURL(bg);
874
- if (url && !bgCache.has(url)) {
875
- promises.push(
876
- fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
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) {