@zumer/snapdom 1.8.0 → 1.9.3

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,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.8.0
4
+ * v.1.9.3
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -164,10 +164,9 @@ async function inlineSingleBackgroundEntry(entry, options = {}) {
164
164
  if (bgCache.has(encodedUrl)) {
165
165
  return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
166
166
  } else {
167
- const crossOrigin = options.crossOrigin ? options.crossOrigin(encodedUrl) : "anonymous";
168
- const dataUrl = await fetchImage(encodedUrl, 3e3, crossOrigin);
167
+ const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
169
168
  bgCache.set(encodedUrl, dataUrl);
170
- return options.skipInline ? void 0 : `url(${dataUrl})`;
169
+ return options.skipInline ? void 0 : `url("${dataUrl}")`;
171
170
  }
172
171
  }
173
172
  if (isGradient || entry === "none") {
@@ -219,30 +218,84 @@ function extractURL(value) {
219
218
  }
220
219
  return url;
221
220
  }
222
- function isIconFont(familyOrUrl) {
223
- const iconFontPatterns = [
224
- /font\s*awesome/i,
225
- /material\s*icons/i,
226
- /ionicons/i,
227
- /glyphicons/i,
228
- /feather/i,
229
- /bootstrap\s*icons/i,
230
- /remix\s*icons/i,
231
- /heroicons/i,
232
- /lucide/i
233
- ];
234
- return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
235
- }
236
- function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
221
+ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
222
+ function getCrossOriginMode(url) {
223
+ try {
224
+ const parsed = new URL(url, window.location.href);
225
+ return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
226
+ } catch {
227
+ return "anonymous";
228
+ }
229
+ }
230
+ async function fetchWithFallback(url) {
231
+ const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
232
+ mode: "cors",
233
+ credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
234
+ }).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
235
+ const reader = new FileReader();
236
+ reader.onloadend = () => {
237
+ const base64 = reader.result;
238
+ if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
239
+ reject(new Error("Invalid image data URL"));
240
+ return;
241
+ }
242
+ resolve(base64);
243
+ };
244
+ reader.onerror = () => reject(new Error("FileReader error"));
245
+ reader.readAsDataURL(blob);
246
+ }));
247
+ try {
248
+ return await fetchBlobAsDataURL(url);
249
+ } catch (e) {
250
+ if (useProxy && typeof useProxy === "string") {
251
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
252
+ try {
253
+ return await fetchBlobAsDataURL(proxied);
254
+ } catch {
255
+ console.error(`[SnapDOM - fetchImage] Proxy fallback failed for: ${url}`);
256
+ throw new Error("CORS restrictions prevented image capture (even via proxy)");
257
+ }
258
+ } else {
259
+ console.error(`[SnapDOM - fetchImage] No valid proxy URL provided for fallback: ${url}`);
260
+ throw new Error("Fetch fallback failed and no proxy provided");
261
+ }
262
+ }
263
+ }
264
+ const crossOriginValue = getCrossOriginMode(src);
265
+ console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
237
266
  if (imageCache.has(src)) {
267
+ console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
238
268
  return Promise.resolve(imageCache.get(src));
239
269
  }
270
+ const isDataURI = src.startsWith("data:image/");
271
+ if (isDataURI) {
272
+ imageCache.set(src, src);
273
+ return Promise.resolve(src);
274
+ }
275
+ const isSVG = /\.svg(\?.*)?$/i.test(src);
276
+ if (isSVG) {
277
+ return (async () => {
278
+ try {
279
+ const response = await fetch(src, {
280
+ mode: "cors",
281
+ credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
282
+ });
283
+ const svgText = await response.text();
284
+ const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
285
+ imageCache.set(src, encoded);
286
+ return encoded;
287
+ } catch {
288
+ return fetchWithFallback(src);
289
+ }
290
+ })();
291
+ }
240
292
  return new Promise((resolve, reject) => {
241
293
  const timeoutId = setTimeout(() => {
294
+ console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
242
295
  reject(new Error("Image load timed out"));
243
296
  }, timeout);
244
297
  const image = new Image();
245
- image.crossOrigin = crossOrigin;
298
+ image.crossOrigin = crossOriginValue;
246
299
  image.onload = async () => {
247
300
  clearTimeout(timeoutId);
248
301
  try {
@@ -252,20 +305,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
252
305
  canvas.height = image.height;
253
306
  const ctx = canvas.getContext("2d");
254
307
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
308
+ const dataURL = canvas.toDataURL("image/png");
309
+ imageCache.set(src, dataURL);
310
+ resolve(dataURL);
311
+ } catch {
255
312
  try {
256
- const dataURL = canvas.toDataURL("image/png");
257
- imageCache.set(src, dataURL);
258
- resolve(dataURL);
313
+ const fallbackDataURL = await fetchWithFallback(src);
314
+ imageCache.set(src, fallbackDataURL);
315
+ resolve(fallbackDataURL);
259
316
  } catch (e) {
260
- reject(new Error("CORS restrictions prevented image capture"));
317
+ reject(e);
261
318
  }
262
- } catch (e) {
263
- reject(e);
264
319
  }
265
320
  };
266
- image.onerror = (e) => {
321
+ image.onerror = async () => {
267
322
  clearTimeout(timeoutId);
268
- reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
323
+ console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
324
+ try {
325
+ const fallbackDataURL = await fetchWithFallback(src);
326
+ imageCache.set(src, fallbackDataURL);
327
+ resolve(fallbackDataURL);
328
+ } catch (e) {
329
+ reject(e);
330
+ }
269
331
  };
270
332
  image.src = src;
271
333
  });
@@ -300,9 +362,9 @@ function stripTranslate(transform) {
300
362
  return cleaned.trim().replace(/\s{2,}/g, " ");
301
363
  }
302
364
  function safeEncodeURI(uri) {
365
+ if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
303
366
  try {
304
- const newURI = decodeURI(uri);
305
- return encodeURI(newURI);
367
+ return encodeURI(uri);
306
368
  } catch {
307
369
  return uri;
308
370
  }
@@ -325,33 +387,91 @@ function splitBackgroundImage(bg) {
325
387
  }
326
388
 
327
389
  // src/modules/styles.js
390
+ var snapshotCache = /* @__PURE__ */ new WeakMap();
391
+ var snapshotKeyCache = /* @__PURE__ */ new Map();
392
+ function snapshotComputedStyleFull(style) {
393
+ const result = {};
394
+ for (let i = 0; i < style.length; i++) {
395
+ const prop = style[i];
396
+ let val = style.getPropertyValue(prop);
397
+ if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
398
+ val = "none";
399
+ }
400
+ result[prop] = val;
401
+ }
402
+ return result;
403
+ }
328
404
  function inlineAllStyles(source, clone, styleMap, cache, compress) {
329
405
  if (source.tagName === "STYLE") return;
330
406
  if (!cache.has(source)) {
331
407
  cache.set(source, getStyle(source));
332
408
  }
333
409
  const style = cache.get(source);
334
- const snapshot = snapshotComputedStyle(style);
410
+ if (!snapshotCache.has(source)) {
411
+ const snapshot2 = snapshotComputedStyleFull(style);
412
+ snapshotCache.set(source, snapshot2);
413
+ }
414
+ const snapshot = snapshotCache.get(source);
415
+ const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
416
+ if (snapshotKeyCache.has(hash)) {
417
+ styleMap.set(clone, snapshotKeyCache.get(hash));
418
+ return;
419
+ }
335
420
  const tagName = source.tagName?.toLowerCase() || "div";
336
421
  const key = getStyleKey(snapshot, tagName, compress);
422
+ snapshotKeyCache.set(hash, key);
337
423
  styleMap.set(clone, key);
338
424
  }
339
425
 
340
426
  // src/core/clone.js
341
- function deepClone(node, styleMap, styleCache, nodeMap, compress) {
342
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
427
+ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
428
+ 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
+ return node.cloneNode(true);
434
+ }
435
+ if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
436
+ if (node.getAttribute("data-capture") === "exclude") {
343
437
  const spacer = document.createElement("div");
344
438
  const rect = node.getBoundingClientRect();
345
439
  spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
346
440
  return spacer;
347
441
  }
442
+ if (options.exclude && Array.isArray(options.exclude) && options.exclude.length > 0) {
443
+ for (const selector of options.exclude) {
444
+ try {
445
+ if (node.matches && node.matches(selector)) {
446
+ const spacer = document.createElement("div");
447
+ const rect = node.getBoundingClientRect();
448
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
449
+ return spacer;
450
+ }
451
+ } catch (err) {
452
+ console.warn(`Invalid selector in exclude option: ${selector}`, err);
453
+ }
454
+ }
455
+ }
456
+ if (typeof options.filter === "function") {
457
+ try {
458
+ if (!options.filter(node, originalRoot || node)) {
459
+ const spacer = document.createElement("div");
460
+ const rect = node.getBoundingClientRect();
461
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
462
+ return spacer;
463
+ }
464
+ } catch (err) {
465
+ console.warn("Error in filter function:", err);
466
+ }
467
+ }
348
468
  if (node.tagName === "IFRAME") {
349
469
  const fallback = document.createElement("div");
350
470
  fallback.textContent = "";
351
471
  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;`;
352
472
  return fallback;
353
473
  }
354
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
474
+ if (node.getAttribute("data-capture") === "placeholder") {
355
475
  const clone2 = node.cloneNode(false);
356
476
  nodeMap.set(clone2, node);
357
477
  inlineAllStyles(node, clone2, styleMap, styleCache, compress);
@@ -372,14 +492,6 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
372
492
  img.style.height = node.style.height || `${node.height}px`;
373
493
  return img;
374
494
  }
375
- if (node.nodeType === Node.TEXT_NODE) {
376
- if (node.parentElement?.shadowRoot) {
377
- const tag = node.parentElement.tagName.toLowerCase();
378
- if (customElements.get(tag)) return null;
379
- }
380
- return node.cloneNode(true);
381
- }
382
- if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
383
495
  const clone = node.cloneNode(false);
384
496
  nodeMap.set(clone, node);
385
497
  if (node instanceof HTMLInputElement) {
@@ -404,13 +516,14 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
404
516
  }
405
517
  inlineAllStyles(node, clone, styleMap, styleCache, compress);
406
518
  const frag = document.createDocumentFragment();
519
+ const rootElement = originalRoot || node;
407
520
  node.childNodes.forEach((child) => {
408
- const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
521
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, rootElement);
409
522
  if (clonedChild) frag.appendChild(clonedChild);
410
523
  });
411
524
  clone.appendChild(frag);
412
525
  if (node.shadowRoot) {
413
- const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap)).filter(Boolean);
526
+ const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap, compress, options, rootElement)).filter(Boolean);
414
527
  const shadowFrag = document.createDocumentFragment();
415
528
  shadowContent.forEach((child) => shadowFrag.appendChild(child));
416
529
  clone.appendChild(shadowFrag);
@@ -418,6 +531,43 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
418
531
  return clone;
419
532
  }
420
533
 
534
+ // src/modules/iconFonts.js
535
+ var defaultIconFonts = [
536
+ // /uicons/i,
537
+ /font\s*awesome/i,
538
+ /material\s*icons/i,
539
+ /ionicons/i,
540
+ /glyphicons/i,
541
+ /feather/i,
542
+ /bootstrap\s*icons/i,
543
+ /remix\s*icons/i,
544
+ /heroicons/i,
545
+ /layui/i,
546
+ /lucide/i
547
+ ];
548
+ var userIconFonts = [];
549
+ function extendIconFonts(fonts) {
550
+ const list = Array.isArray(fonts) ? fonts : [fonts];
551
+ for (const f of list) {
552
+ if (f instanceof RegExp) {
553
+ userIconFonts.push(f);
554
+ } else if (typeof f === "string") {
555
+ userIconFonts.push(new RegExp(f, "i"));
556
+ } else {
557
+ console.warn("[snapdom] Ignored invalid iconFont value:", f);
558
+ }
559
+ }
560
+ }
561
+ function isIconFont(input) {
562
+ const text = typeof input === "string" ? input : "";
563
+ const candidates = [...defaultIconFonts, ...userIconFonts];
564
+ for (const rx of candidates) {
565
+ if (rx instanceof RegExp && rx.test(text)) return true;
566
+ }
567
+ if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
568
+ return false;
569
+ }
570
+
421
571
  // src/modules/fonts.js
422
572
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
423
573
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
@@ -457,7 +607,7 @@ function injectLinkIfMissing(href) {
457
607
  document.head.appendChild(link);
458
608
  });
459
609
  }
460
- async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
610
+ async function embedCustomFonts({ preCached = false } = {}) {
461
611
  if (resourceCache.has("fonts-embed-css")) {
462
612
  if (preCached) {
463
613
  const style = document.createElement("style");
@@ -474,6 +624,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
474
624
  const matches = Array.from(cssText.matchAll(importRegex));
475
625
  for (const match of matches) {
476
626
  const importUrl = match[1];
627
+ if (isIconFont(importUrl)) continue;
477
628
  if (!isStylesheetLoaded(importUrl)) {
478
629
  styleImports.push(importUrl);
479
630
  }
@@ -486,7 +637,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
486
637
  try {
487
638
  const res = await fetch(link.href);
488
639
  const cssText = await res.text();
489
- if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) continue;
640
+ if (isIconFont(link.href) || isIconFont(cssText)) continue;
490
641
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
491
642
  const inlinedCSS = await Promise.all(
492
643
  Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
@@ -496,7 +647,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
496
647
  if (!url.startsWith("http") && !url.startsWith("data:")) {
497
648
  url = new URL(url, link.href).href;
498
649
  }
499
- if (ignoreIconFonts && isIconFont(url)) return null;
650
+ if (isIconFont(url)) return null;
500
651
  if (resourceCache.has(url)) {
501
652
  processedFontURLs.add(url);
502
653
  return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
@@ -534,7 +685,8 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
534
685
  for (const rule of sheet.cssRules) {
535
686
  if (rule.type === CSSRule.FONT_FACE_RULE) {
536
687
  const src = rule.style.getPropertyValue("src");
537
- if (!src) continue;
688
+ const family = rule.style.getPropertyValue("font-family");
689
+ if (!src || isIconFont(family)) continue;
538
690
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
539
691
  let inlinedSrc = src;
540
692
  const matches = Array.from(src.matchAll(urlRegex));
@@ -545,7 +697,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
545
697
  if (!url.startsWith("http") && !url.startsWith("data:")) {
546
698
  url = new URL(url, sheet.href || location.href).href;
547
699
  }
548
- if (ignoreIconFonts && isIconFont(url)) continue;
700
+ if (isIconFont(url)) continue;
549
701
  if (resourceCache.has(url)) {
550
702
  processedFontURLs.add(url);
551
703
  inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
@@ -568,7 +720,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
568
720
  }
569
721
  }
570
722
  finalCSS += `@font-face {
571
- font-family: ${rule.style.getPropertyValue("font-family")};
723
+ font-family: ${family};
572
724
  src: ${inlinedSrc};
573
725
  font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
574
726
  font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
@@ -583,6 +735,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
583
735
  }
584
736
  for (const font of document.fonts) {
585
737
  if (font.family && font.status === "loaded" && font._snapdomSrc) {
738
+ if (isIconFont(font.family)) continue;
586
739
  let b64 = font._snapdomSrc;
587
740
  if (!b64.startsWith("data:")) {
588
741
  if (resourceCache.has(font._snapdomSrc)) {
@@ -627,7 +780,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
627
780
  }
628
781
 
629
782
  // src/modules/pseudo.js
630
- async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
783
+ async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
631
784
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
632
785
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
633
786
  try {
@@ -673,23 +826,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
673
826
  const snapshot = snapshotComputedStyle(style);
674
827
  const key = getStyleKey(snapshot, "span", compress);
675
828
  styleMap.set(pseudoEl, key);
676
- const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons|lucide/i.test(
677
- fontFamily
678
- );
829
+ const isIconFont2 = isIconFont(fontFamily);
679
830
  const cleanContent = parseContent(content);
680
- if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
831
+ if (isIconFont2 && cleanContent.length === 1) {
681
832
  const imgEl = document.createElement("img");
682
833
  imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
683
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
834
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
684
835
  pseudoEl.appendChild(imgEl);
685
836
  } else if (cleanContent.startsWith("url(")) {
686
837
  const rawUrl = extractURL(cleanContent);
687
838
  if (rawUrl && rawUrl.trim() !== "") {
688
839
  try {
689
840
  const imgEl = document.createElement("img");
690
- const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
841
+ const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
691
842
  imgEl.src = dataUrl;
692
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
843
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
693
844
  pseudoEl.appendChild(imgEl);
694
845
  } catch (e) {
695
846
  console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
@@ -728,28 +879,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
728
879
  styleMap,
729
880
  styleCache,
730
881
  compress,
731
- embedFonts
882
+ embedFonts,
883
+ useProxy
732
884
  );
733
885
  }
734
886
  }
735
887
 
888
+ // src/modules/svgDefs.js
889
+ function inlineExternalDef(root) {
890
+ if (!root) return;
891
+ const defsSources = document.querySelectorAll("svg > defs");
892
+ if (!defsSources.length) return;
893
+ root.querySelectorAll("svg").forEach((svg) => {
894
+ const uses = svg.querySelectorAll("use");
895
+ if (!uses.length) return;
896
+ const usedIds = /* @__PURE__ */ new Set();
897
+ uses.forEach((use) => {
898
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
899
+ if (href && href.startsWith("#")) usedIds.add(href.slice(1));
900
+ });
901
+ if (!usedIds.size) return;
902
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
903
+ for (const id of usedIds) {
904
+ for (const source of defsSources) {
905
+ const def = source.querySelector(`#${CSS.escape(id)}`);
906
+ if (def) {
907
+ defs.appendChild(def.cloneNode(true));
908
+ break;
909
+ }
910
+ }
911
+ }
912
+ if (defs.childNodes.length) {
913
+ svg.insertBefore(defs, svg.firstChild);
914
+ }
915
+ });
916
+ }
917
+
736
918
  // src/core/prepare.js
737
- async function prepareClone(element, compress = false, embedFonts = false) {
919
+ async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
738
920
  const styleMap = /* @__PURE__ */ new Map();
739
921
  const styleCache = /* @__PURE__ */ new WeakMap();
740
922
  const nodeMap = /* @__PURE__ */ new Map();
741
923
  let clone;
742
924
  try {
743
- clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
925
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
744
926
  } catch (e) {
745
927
  console.warn("deepClone failed:", e);
746
928
  throw e;
747
929
  }
748
930
  try {
749
- await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
931
+ await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
750
932
  } catch (e) {
751
933
  console.warn("inlinePseudoElements failed:", e);
752
934
  }
935
+ try {
936
+ inlineExternalDef(clone);
937
+ } catch (e) {
938
+ console.warn("inlineExternalDef failed:", e);
939
+ }
753
940
  let classCSS = "";
754
941
  if (compress) {
755
942
  const keyToClass = generateCSSClasses(styleMap);
@@ -802,6 +989,12 @@ async function prepareClone(element, compress = false, embedFonts = false) {
802
989
  clone.style.clear = "none";
803
990
  clone.style.transform = transform || "";
804
991
  }
992
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
993
+ if (originalNode.tagName === "PRE") {
994
+ cloneNode.style.marginTop = "0";
995
+ cloneNode.style.marginBlockStart = "0";
996
+ }
997
+ }
805
998
  return { clone, classCSS, styleCache };
806
999
  }
807
1000
 
@@ -811,8 +1004,7 @@ async function inlineImages(clone, options = {}) {
811
1004
  const processImg = async (img) => {
812
1005
  const src = img.src;
813
1006
  try {
814
- const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
815
- const dataUrl = await fetchImage(src, 3e3, crossOrigin);
1007
+ const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
816
1008
  img.src = dataUrl;
817
1009
  if (!img.width) img.width = img.naturalWidth || 100;
818
1010
  if (!img.height) img.height = img.naturalHeight || 100;
@@ -850,10 +1042,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
850
1042
  const newBgParts = await Promise.all(
851
1043
  bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
852
1044
  );
853
- const hasRealBg = newBgParts.some(
854
- (p) => p && p !== "none" && !/^url\(undefined\)/.test(p)
855
- );
856
- if (hasRealBg) {
1045
+ if (newBgParts.some((p) => p && p !== "none" && !/^url\(undefined\)/.test(p))) {
857
1046
  cloneNode.style.backgroundImage = newBgParts.join(", ");
858
1047
  }
859
1048
  if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
@@ -870,14 +1059,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
870
1059
  // src/core/capture.js
871
1060
  async function captureDOM(element, options = {}) {
872
1061
  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");
874
- const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
1062
+ const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
875
1063
  let clone, classCSS, styleCache;
876
1064
  let fontsCSS = "";
877
1065
  let baseCSS = "";
878
1066
  let dataURL;
879
1067
  let svgString;
880
- ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
1068
+ ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
881
1069
  await new Promise((resolve) => {
882
1070
  idle(async () => {
883
1071
  await inlineImages(clone, options);
@@ -893,7 +1081,7 @@ async function captureDOM(element, options = {}) {
893
1081
  if (embedFonts) {
894
1082
  await new Promise((resolve) => {
895
1083
  idle(async () => {
896
- fontsCSS = await embedCustomFonts({ ignoreIconFonts: !embedFonts });
1084
+ fontsCSS = await embedCustomFonts();
897
1085
  resolve();
898
1086
  }, { fast });
899
1087
  });
@@ -1001,9 +1189,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
1001
1189
  canvas.style.height = `${height}px`;
1002
1190
  return canvas;
1003
1191
  }
1004
- async function toBlob(url) {
1005
- const svgText = decodeURIComponent(url.split(",")[1]);
1006
- return new Blob([svgText], { type: "image/svg+xml" });
1192
+ async function toBlob(url, {
1193
+ type = "svg",
1194
+ scale = 1,
1195
+ backgroundColor = "#fff",
1196
+ quality
1197
+ } = {}) {
1198
+ const mime = {
1199
+ jpg: "image/jpeg",
1200
+ jpeg: "image/jpeg",
1201
+ png: "image/png",
1202
+ webp: "image/webp"
1203
+ }[type] || "image/png";
1204
+ if (type === "svg") {
1205
+ const svgText = decodeURIComponent(url.split(",")[1]);
1206
+ return new Blob([svgText], { type: "image/svg+xml" });
1207
+ }
1208
+ const canvas = await createBackground(url, { dpr: 1, scale }, backgroundColor);
1209
+ return new Promise((resolve) => {
1210
+ canvas.toBlob((blob) => resolve(blob), `${mime}`, quality);
1211
+ });
1007
1212
  }
1008
1213
  async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
1009
1214
  const baseCanvas = await toCanvas(url, { dpr, scale });
@@ -1055,6 +1260,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
1055
1260
  async function snapdom(element, options = {}) {
1056
1261
  options = { scale: 1, ...options };
1057
1262
  if (!element) throw new Error("Element cannot be null or undefined");
1263
+ if (options.iconFonts) {
1264
+ extendIconFonts(options.iconFonts);
1265
+ }
1058
1266
  return await snapdom.capture(element, options);
1059
1267
  }
1060
1268
  snapdom.capture = async (el, options = {}) => {
@@ -1067,7 +1275,7 @@ snapdom.capture = async (el, options = {}) => {
1067
1275
  toRaw: () => url,
1068
1276
  toImg: () => toImg(url, { dpr, scale }),
1069
1277
  toCanvas: () => toCanvas(url, { dpr, scale }),
1070
- toBlob: () => toBlob(url),
1278
+ toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
1071
1279
  toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
1072
1280
  toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
1073
1281
  toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
@@ -1077,7 +1285,7 @@ snapdom.capture = async (el, options = {}) => {
1077
1285
  snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
1078
1286
  snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
1079
1287
  snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
1080
- snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
1288
+ snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
1081
1289
  snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
1082
1290
  snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
1083
1291
  snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
@@ -1094,7 +1302,7 @@ snapdom.download = async (el, options = {}) => {
1094
1302
 
1095
1303
  // src/api/preCache.js
1096
1304
  async function preCache(root = document, options = {}) {
1097
- const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
1305
+ const { embedFonts = true, reset = false } = options;
1098
1306
  if (reset) {
1099
1307
  imageCache.clear();
1100
1308
  bgCache.clear();
@@ -1113,9 +1321,8 @@ async function preCache(root = document, options = {}) {
1113
1321
  for (const img of imgEls) {
1114
1322
  const src = img.src;
1115
1323
  if (!imageCache.has(src)) {
1116
- const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
1117
1324
  promises.push(
1118
- fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
1325
+ fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
1119
1326
  })
1120
1327
  );
1121
1328
  }
@@ -1128,7 +1335,7 @@ async function preCache(root = document, options = {}) {
1128
1335
  const isUrl = entry.startsWith("url(");
1129
1336
  if (isUrl) {
1130
1337
  promises.push(
1131
- inlineSingleBackgroundEntry(entry, { crossOrigin: crossOriginFn, skipInline: true }).catch(() => {
1338
+ inlineSingleBackgroundEntry(entry, options).catch(() => {
1132
1339
  })
1133
1340
  );
1134
1341
  }
@@ -1136,7 +1343,7 @@ async function preCache(root = document, options = {}) {
1136
1343
  }
1137
1344
  }
1138
1345
  if (embedFonts) {
1139
- await embedCustomFonts({ ignoreIconFonts: !embedFonts, preCached: true });
1346
+ await embedCustomFonts({ preCached: true });
1140
1347
  }
1141
1348
  await Promise.all(promises);
1142
1349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.8.0",
3
+ "version": "1.9.3",
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",