@zumer/snapdom 1.8.0 → 1.9.3-next-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-next-dev.0
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -155,19 +155,17 @@ function generateCSSClasses(styleMap) {
155
155
 
156
156
  // src/utils/helpers.js
157
157
  async function inlineSingleBackgroundEntry(entry, options = {}) {
158
- const isUrl = entry.startsWith("url(");
158
+ const match = entry.match(/url\(["']?(.*?)["']?\)/);
159
+ const rawUrl = match?.[1];
159
160
  const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
160
- if (isUrl) {
161
- const rawUrl = extractURL(entry);
162
- if (!rawUrl) return entry;
161
+ if (rawUrl) {
163
162
  const encodedUrl = safeEncodeURI(rawUrl);
164
163
  if (bgCache.has(encodedUrl)) {
165
164
  return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
166
165
  } else {
167
- const crossOrigin = options.crossOrigin ? options.crossOrigin(encodedUrl) : "anonymous";
168
- const dataUrl = await fetchImage(encodedUrl, 3e3, crossOrigin);
166
+ const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
169
167
  bgCache.set(encodedUrl, dataUrl);
170
- return options.skipInline ? void 0 : `url(${dataUrl})`;
168
+ return options.skipInline ? void 0 : `url("${dataUrl}")`;
171
169
  }
172
170
  }
173
171
  if (isGradient || entry === "none") {
@@ -219,30 +217,84 @@ function extractURL(value) {
219
217
  }
220
218
  return url;
221
219
  }
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") {
220
+ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
221
+ function getCrossOriginMode(url) {
222
+ try {
223
+ const parsed = new URL(url, window.location.href);
224
+ return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
225
+ } catch {
226
+ return "anonymous";
227
+ }
228
+ }
229
+ async function fetchWithFallback(url) {
230
+ const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
231
+ mode: "cors",
232
+ credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
233
+ }).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
234
+ const reader = new FileReader();
235
+ reader.onloadend = () => {
236
+ const base64 = reader.result;
237
+ if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
238
+ reject(new Error("Invalid image data URL"));
239
+ return;
240
+ }
241
+ resolve(base64);
242
+ };
243
+ reader.onerror = () => reject(new Error("FileReader error"));
244
+ reader.readAsDataURL(blob);
245
+ }));
246
+ try {
247
+ return await fetchBlobAsDataURL(url);
248
+ } catch (e) {
249
+ if (useProxy && typeof useProxy === "string") {
250
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
251
+ try {
252
+ return await fetchBlobAsDataURL(proxied);
253
+ } catch {
254
+ console.error(`[SnapDOM - fetchImage] Proxy fallback failed for: ${url}`);
255
+ throw new Error("CORS restrictions prevented image capture (even via proxy)");
256
+ }
257
+ } else {
258
+ console.error(`[SnapDOM - fetchImage] No valid proxy URL provided for fallback: ${url}`);
259
+ throw new Error("Fetch fallback failed and no proxy provided");
260
+ }
261
+ }
262
+ }
263
+ const crossOriginValue = getCrossOriginMode(src);
264
+ console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
237
265
  if (imageCache.has(src)) {
266
+ console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
238
267
  return Promise.resolve(imageCache.get(src));
239
268
  }
269
+ const isDataURI = src.startsWith("data:image/");
270
+ if (isDataURI) {
271
+ imageCache.set(src, src);
272
+ return Promise.resolve(src);
273
+ }
274
+ const isSVG = /\.svg(\?.*)?$/i.test(src);
275
+ if (isSVG) {
276
+ return (async () => {
277
+ try {
278
+ const response = await fetch(src, {
279
+ mode: "cors",
280
+ credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
281
+ });
282
+ const svgText = await response.text();
283
+ const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
284
+ imageCache.set(src, encoded);
285
+ return encoded;
286
+ } catch {
287
+ return fetchWithFallback(src);
288
+ }
289
+ })();
290
+ }
240
291
  return new Promise((resolve, reject) => {
241
292
  const timeoutId = setTimeout(() => {
293
+ console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
242
294
  reject(new Error("Image load timed out"));
243
295
  }, timeout);
244
296
  const image = new Image();
245
- image.crossOrigin = crossOrigin;
297
+ image.crossOrigin = crossOriginValue;
246
298
  image.onload = async () => {
247
299
  clearTimeout(timeoutId);
248
300
  try {
@@ -252,20 +304,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
252
304
  canvas.height = image.height;
253
305
  const ctx = canvas.getContext("2d");
254
306
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
307
+ const dataURL = canvas.toDataURL("image/png");
308
+ imageCache.set(src, dataURL);
309
+ resolve(dataURL);
310
+ } catch {
255
311
  try {
256
- const dataURL = canvas.toDataURL("image/png");
257
- imageCache.set(src, dataURL);
258
- resolve(dataURL);
312
+ const fallbackDataURL = await fetchWithFallback(src);
313
+ imageCache.set(src, fallbackDataURL);
314
+ resolve(fallbackDataURL);
259
315
  } catch (e) {
260
- reject(new Error("CORS restrictions prevented image capture"));
316
+ reject(e);
261
317
  }
262
- } catch (e) {
263
- reject(e);
264
318
  }
265
319
  };
266
- image.onerror = (e) => {
320
+ image.onerror = async () => {
267
321
  clearTimeout(timeoutId);
268
- reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
322
+ console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
323
+ try {
324
+ const fallbackDataURL = await fetchWithFallback(src);
325
+ imageCache.set(src, fallbackDataURL);
326
+ resolve(fallbackDataURL);
327
+ } catch (e) {
328
+ reject(e);
329
+ }
269
330
  };
270
331
  image.src = src;
271
332
  });
@@ -300,9 +361,9 @@ function stripTranslate(transform) {
300
361
  return cleaned.trim().replace(/\s{2,}/g, " ");
301
362
  }
302
363
  function safeEncodeURI(uri) {
364
+ if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
303
365
  try {
304
- const newURI = decodeURI(uri);
305
- return encodeURI(newURI);
366
+ return encodeURI(uri);
306
367
  } catch {
307
368
  return uri;
308
369
  }
@@ -325,33 +386,90 @@ function splitBackgroundImage(bg) {
325
386
  }
326
387
 
327
388
  // src/modules/styles.js
389
+ var snapshotCache = /* @__PURE__ */ new WeakMap();
390
+ var snapshotKeyCache = /* @__PURE__ */ new Map();
391
+ function snapshotComputedStyleFull(style) {
392
+ const result = {};
393
+ for (let i = 0; i < style.length; i++) {
394
+ const prop = style[i];
395
+ let val = style.getPropertyValue(prop);
396
+ if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
397
+ val = "none";
398
+ }
399
+ result[prop] = val;
400
+ }
401
+ return result;
402
+ }
328
403
  function inlineAllStyles(source, clone, styleMap, cache, compress) {
329
404
  if (source.tagName === "STYLE") return;
330
405
  if (!cache.has(source)) {
331
406
  cache.set(source, getStyle(source));
332
407
  }
333
408
  const style = cache.get(source);
334
- const snapshot = snapshotComputedStyle(style);
409
+ if (!snapshotCache.has(source)) {
410
+ const snapshot2 = snapshotComputedStyleFull(style);
411
+ snapshotCache.set(source, snapshot2);
412
+ }
413
+ const snapshot = snapshotCache.get(source);
414
+ const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
415
+ if (snapshotKeyCache.has(hash)) {
416
+ styleMap.set(clone, snapshotKeyCache.get(hash));
417
+ return;
418
+ }
335
419
  const tagName = source.tagName?.toLowerCase() || "div";
336
420
  const key = getStyleKey(snapshot, tagName, compress);
421
+ snapshotKeyCache.set(hash, key);
337
422
  styleMap.set(clone, key);
338
423
  }
339
424
 
340
425
  // src/core/clone.js
341
- function deepClone(node, styleMap, styleCache, nodeMap, compress) {
342
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
426
+ function isSlotElement(node) {
427
+ return node.nodeType === Node.ELEMENT_NODE && node.tagName === "SLOT";
428
+ }
429
+ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
430
+ if (node.nodeType === Node.TEXT_NODE) {
431
+ return node.cloneNode(true);
432
+ }
433
+ if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
434
+ if (node.getAttribute("data-capture") === "exclude") {
343
435
  const spacer = document.createElement("div");
344
436
  const rect = node.getBoundingClientRect();
345
437
  spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
346
438
  return spacer;
347
439
  }
440
+ if (options.exclude && Array.isArray(options.exclude)) {
441
+ for (const selector of options.exclude) {
442
+ try {
443
+ if (node.matches?.(selector)) {
444
+ const spacer = document.createElement("div");
445
+ const rect = node.getBoundingClientRect();
446
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
447
+ return spacer;
448
+ }
449
+ } catch (err) {
450
+ console.warn(`Invalid selector in exclude option: ${selector}`, err);
451
+ }
452
+ }
453
+ }
454
+ if (typeof options.filter === "function") {
455
+ try {
456
+ if (!options.filter(node, originalRoot || node)) {
457
+ const spacer = document.createElement("div");
458
+ const rect = node.getBoundingClientRect();
459
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
460
+ return spacer;
461
+ }
462
+ } catch (err) {
463
+ console.warn("Error in filter function:", err);
464
+ }
465
+ }
348
466
  if (node.tagName === "IFRAME") {
349
467
  const fallback = document.createElement("div");
350
468
  fallback.textContent = "";
351
469
  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
470
  return fallback;
353
471
  }
354
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
472
+ if (node.getAttribute("data-capture") === "placeholder") {
355
473
  const clone2 = node.cloneNode(false);
356
474
  nodeMap.set(clone2, node);
357
475
  inlineAllStyles(node, clone2, styleMap, styleCache, compress);
@@ -372,14 +490,6 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
372
490
  img.style.height = node.style.height || `${node.height}px`;
373
491
  return img;
374
492
  }
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
493
  const clone = node.cloneNode(false);
384
494
  nodeMap.set(clone, node);
385
495
  if (node instanceof HTMLInputElement) {
@@ -390,8 +500,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
390
500
  if (node.checked) clone.setAttribute("checked", "");
391
501
  }
392
502
  } else if (node instanceof HTMLTextAreaElement) {
393
- clone.value = node.value;
503
+ const rect = node.getBoundingClientRect();
394
504
  clone.textContent = node.value;
505
+ clone.style.width = `${rect.width}px`;
506
+ clone.style.height = `${rect.height}px`;
395
507
  } else if (node instanceof HTMLSelectElement) {
396
508
  clone.value = node.value;
397
509
  Array.from(clone.options).forEach((opt) => {
@@ -403,21 +515,71 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
403
515
  });
404
516
  }
405
517
  inlineAllStyles(node, clone, styleMap, styleCache, compress);
406
- const frag = document.createDocumentFragment();
407
- node.childNodes.forEach((child) => {
408
- const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
409
- if (clonedChild) frag.appendChild(clonedChild);
410
- });
411
- clone.appendChild(frag);
412
- 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);
414
- const shadowFrag = document.createDocumentFragment();
415
- shadowContent.forEach((child) => shadowFrag.appendChild(child));
416
- clone.appendChild(shadowFrag);
518
+ if (isSlotElement(node)) {
519
+ const assigned = node.assignedNodes?.({ flatten: true }) || [];
520
+ const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
521
+ const fragment = document.createDocumentFragment();
522
+ for (const child of nodesToClone) {
523
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
524
+ if (clonedChild) fragment.appendChild(clonedChild);
525
+ }
526
+ return fragment;
527
+ } else if (node instanceof HTMLTextAreaElement) {
528
+ } else {
529
+ const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
530
+ for (const child of baseChildren) {
531
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
532
+ if (clonedChild) clone.appendChild(clonedChild);
533
+ }
534
+ if (node.shadowRoot && node.childNodes.length > 0 && !node.shadowRoot.querySelector("slot")) {
535
+ const lightDomContent = document.createDocumentFragment();
536
+ for (const child of node.childNodes) {
537
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
538
+ if (clonedChild) lightDomContent.appendChild(clonedChild);
539
+ }
540
+ clone.appendChild(lightDomContent);
541
+ }
417
542
  }
418
543
  return clone;
419
544
  }
420
545
 
546
+ // src/modules/iconFonts.js
547
+ var defaultIconFonts = [
548
+ // /uicons/i,
549
+ /font\s*awesome/i,
550
+ /material\s*icons/i,
551
+ /ionicons/i,
552
+ /glyphicons/i,
553
+ /feather/i,
554
+ /bootstrap\s*icons/i,
555
+ /remix\s*icons/i,
556
+ /heroicons/i,
557
+ /layui/i,
558
+ /lucide/i
559
+ ];
560
+ var userIconFonts = [];
561
+ function extendIconFonts(fonts) {
562
+ const list = Array.isArray(fonts) ? fonts : [fonts];
563
+ for (const f of list) {
564
+ if (f instanceof RegExp) {
565
+ userIconFonts.push(f);
566
+ } else if (typeof f === "string") {
567
+ userIconFonts.push(new RegExp(f, "i"));
568
+ } else {
569
+ console.warn("[snapdom] Ignored invalid iconFont value:", f);
570
+ }
571
+ }
572
+ }
573
+ function isIconFont(input) {
574
+ const text = typeof input === "string" ? input : "";
575
+ const candidates = [...defaultIconFonts, ...userIconFonts];
576
+ for (const rx of candidates) {
577
+ if (rx instanceof RegExp && rx.test(text)) return true;
578
+ }
579
+ if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
580
+ return false;
581
+ }
582
+
421
583
  // src/modules/fonts.js
422
584
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
423
585
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
@@ -457,7 +619,7 @@ function injectLinkIfMissing(href) {
457
619
  document.head.appendChild(link);
458
620
  });
459
621
  }
460
- async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
622
+ async function embedCustomFonts({ preCached = false } = {}) {
461
623
  if (resourceCache.has("fonts-embed-css")) {
462
624
  if (preCached) {
463
625
  const style = document.createElement("style");
@@ -474,6 +636,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
474
636
  const matches = Array.from(cssText.matchAll(importRegex));
475
637
  for (const match of matches) {
476
638
  const importUrl = match[1];
639
+ if (isIconFont(importUrl)) continue;
477
640
  if (!isStylesheetLoaded(importUrl)) {
478
641
  styleImports.push(importUrl);
479
642
  }
@@ -486,7 +649,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
486
649
  try {
487
650
  const res = await fetch(link.href);
488
651
  const cssText = await res.text();
489
- if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) continue;
652
+ if (isIconFont(link.href) || isIconFont(cssText)) continue;
490
653
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
491
654
  const inlinedCSS = await Promise.all(
492
655
  Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
@@ -496,7 +659,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
496
659
  if (!url.startsWith("http") && !url.startsWith("data:")) {
497
660
  url = new URL(url, link.href).href;
498
661
  }
499
- if (ignoreIconFonts && isIconFont(url)) return null;
662
+ if (isIconFont(url)) return null;
500
663
  if (resourceCache.has(url)) {
501
664
  processedFontURLs.add(url);
502
665
  return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
@@ -534,7 +697,8 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
534
697
  for (const rule of sheet.cssRules) {
535
698
  if (rule.type === CSSRule.FONT_FACE_RULE) {
536
699
  const src = rule.style.getPropertyValue("src");
537
- if (!src) continue;
700
+ const family = rule.style.getPropertyValue("font-family");
701
+ if (!src || isIconFont(family)) continue;
538
702
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
539
703
  let inlinedSrc = src;
540
704
  const matches = Array.from(src.matchAll(urlRegex));
@@ -545,7 +709,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
545
709
  if (!url.startsWith("http") && !url.startsWith("data:")) {
546
710
  url = new URL(url, sheet.href || location.href).href;
547
711
  }
548
- if (ignoreIconFonts && isIconFont(url)) continue;
712
+ if (isIconFont(url)) continue;
549
713
  if (resourceCache.has(url)) {
550
714
  processedFontURLs.add(url);
551
715
  inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
@@ -568,7 +732,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
568
732
  }
569
733
  }
570
734
  finalCSS += `@font-face {
571
- font-family: ${rule.style.getPropertyValue("font-family")};
735
+ font-family: ${family};
572
736
  src: ${inlinedSrc};
573
737
  font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
574
738
  font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
@@ -583,6 +747,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
583
747
  }
584
748
  for (const font of document.fonts) {
585
749
  if (font.family && font.status === "loaded" && font._snapdomSrc) {
750
+ if (isIconFont(font.family)) continue;
586
751
  let b64 = font._snapdomSrc;
587
752
  if (!b64.startsWith("data:")) {
588
753
  if (resourceCache.has(font._snapdomSrc)) {
@@ -627,7 +792,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
627
792
  }
628
793
 
629
794
  // src/modules/pseudo.js
630
- async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
795
+ async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
631
796
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
632
797
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
633
798
  try {
@@ -673,23 +838,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
673
838
  const snapshot = snapshotComputedStyle(style);
674
839
  const key = getStyleKey(snapshot, "span", compress);
675
840
  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
- );
841
+ const isIconFont2 = isIconFont(fontFamily);
679
842
  const cleanContent = parseContent(content);
680
- if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
843
+ if (isIconFont2 && cleanContent.length === 1) {
681
844
  const imgEl = document.createElement("img");
682
845
  imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
683
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
846
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
684
847
  pseudoEl.appendChild(imgEl);
685
848
  } else if (cleanContent.startsWith("url(")) {
686
849
  const rawUrl = extractURL(cleanContent);
687
850
  if (rawUrl && rawUrl.trim() !== "") {
688
851
  try {
689
852
  const imgEl = document.createElement("img");
690
- const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
853
+ const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
691
854
  imgEl.src = dataUrl;
692
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
855
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
693
856
  pseudoEl.appendChild(imgEl);
694
857
  } catch (e) {
695
858
  console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
@@ -728,28 +891,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
728
891
  styleMap,
729
892
  styleCache,
730
893
  compress,
731
- embedFonts
894
+ embedFonts,
895
+ useProxy
732
896
  );
733
897
  }
734
898
  }
735
899
 
900
+ // src/modules/svgDefs.js
901
+ function inlineExternalDef(root) {
902
+ if (!root) return;
903
+ const defsSources = document.querySelectorAll("svg > defs");
904
+ if (!defsSources.length) return;
905
+ root.querySelectorAll("svg").forEach((svg) => {
906
+ const uses = svg.querySelectorAll("use");
907
+ if (!uses.length) return;
908
+ const usedIds = /* @__PURE__ */ new Set();
909
+ uses.forEach((use) => {
910
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
911
+ if (href && href.startsWith("#")) usedIds.add(href.slice(1));
912
+ });
913
+ if (!usedIds.size) return;
914
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
915
+ for (const id of usedIds) {
916
+ for (const source of defsSources) {
917
+ const def = source.querySelector(`#${CSS.escape(id)}`);
918
+ if (def) {
919
+ defs.appendChild(def.cloneNode(true));
920
+ break;
921
+ }
922
+ }
923
+ }
924
+ if (defs.childNodes.length) {
925
+ svg.insertBefore(defs, svg.firstChild);
926
+ }
927
+ });
928
+ }
929
+
736
930
  // src/core/prepare.js
737
- async function prepareClone(element, compress = false, embedFonts = false) {
931
+ async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
738
932
  const styleMap = /* @__PURE__ */ new Map();
739
933
  const styleCache = /* @__PURE__ */ new WeakMap();
740
934
  const nodeMap = /* @__PURE__ */ new Map();
741
935
  let clone;
742
936
  try {
743
- clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
937
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
744
938
  } catch (e) {
745
939
  console.warn("deepClone failed:", e);
746
940
  throw e;
747
941
  }
748
942
  try {
749
- await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
943
+ await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
750
944
  } catch (e) {
751
945
  console.warn("inlinePseudoElements failed:", e);
752
946
  }
947
+ try {
948
+ inlineExternalDef(clone);
949
+ } catch (e) {
950
+ console.warn("inlineExternalDef failed:", e);
951
+ }
753
952
  let classCSS = "";
754
953
  if (compress) {
755
954
  const keyToClass = generateCSSClasses(styleMap);
@@ -802,6 +1001,12 @@ async function prepareClone(element, compress = false, embedFonts = false) {
802
1001
  clone.style.clear = "none";
803
1002
  clone.style.transform = transform || "";
804
1003
  }
1004
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
1005
+ if (originalNode.tagName === "PRE") {
1006
+ cloneNode.style.marginTop = "0";
1007
+ cloneNode.style.marginBlockStart = "0";
1008
+ }
1009
+ }
805
1010
  return { clone, classCSS, styleCache };
806
1011
  }
807
1012
 
@@ -811,8 +1016,7 @@ async function inlineImages(clone, options = {}) {
811
1016
  const processImg = async (img) => {
812
1017
  const src = img.src;
813
1018
  try {
814
- const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
815
- const dataUrl = await fetchImage(src, 3e3, crossOrigin);
1019
+ const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
816
1020
  img.src = dataUrl;
817
1021
  if (!img.width) img.width = img.naturalWidth || 100;
818
1022
  if (!img.height) img.height = img.naturalHeight || 100;
@@ -832,30 +1036,32 @@ async function inlineImages(clone, options = {}) {
832
1036
  // src/modules/background.js
833
1037
  async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
834
1038
  const queue = [[source, clone]];
1039
+ const imageProps = [
1040
+ "background-image",
1041
+ "mask",
1042
+ "mask-image",
1043
+ "-webkit-mask-image",
1044
+ "mask-source",
1045
+ "mask-box-image-source",
1046
+ "mask-border-source",
1047
+ "-webkit-mask-box-image-source"
1048
+ ];
835
1049
  while (queue.length) {
836
1050
  const [srcNode, cloneNode] = queue.shift();
837
1051
  const style = styleCache.get(srcNode) || getStyle(srcNode);
838
1052
  if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
839
- const bg = style.getPropertyValue("background-image");
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]]);
1053
+ for (const prop of imageProps) {
1054
+ const val = style.getPropertyValue(prop);
1055
+ if (!val || val === "none") continue;
1056
+ const splits = splitBackgroundImage(val);
1057
+ const inlined = await Promise.all(
1058
+ splits.map((entry) => inlineSingleBackgroundEntry(entry, options))
1059
+ );
1060
+ if (inlined.some((p) => p && p !== "none" && !/^url\(undefined/.test(p))) {
1061
+ cloneNode.style.setProperty(prop, inlined.join(", "));
846
1062
  }
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
1063
  }
1064
+ const bgColor = style.getPropertyValue("background-color");
859
1065
  if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
860
1066
  cloneNode.style.backgroundColor = bgColor;
861
1067
  }
@@ -870,14 +1076,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
870
1076
  // src/core/capture.js
871
1077
  async function captureDOM(element, options = {}) {
872
1078
  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;
1079
+ const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
875
1080
  let clone, classCSS, styleCache;
876
1081
  let fontsCSS = "";
877
1082
  let baseCSS = "";
878
1083
  let dataURL;
879
1084
  let svgString;
880
- ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
1085
+ ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
881
1086
  await new Promise((resolve) => {
882
1087
  idle(async () => {
883
1088
  await inlineImages(clone, options);
@@ -893,7 +1098,7 @@ async function captureDOM(element, options = {}) {
893
1098
  if (embedFonts) {
894
1099
  await new Promise((resolve) => {
895
1100
  idle(async () => {
896
- fontsCSS = await embedCustomFonts({ ignoreIconFonts: !embedFonts });
1101
+ fontsCSS = await embedCustomFonts();
897
1102
  resolve();
898
1103
  }, { fast });
899
1104
  });
@@ -1001,9 +1206,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
1001
1206
  canvas.style.height = `${height}px`;
1002
1207
  return canvas;
1003
1208
  }
1004
- async function toBlob(url) {
1005
- const svgText = decodeURIComponent(url.split(",")[1]);
1006
- return new Blob([svgText], { type: "image/svg+xml" });
1209
+ async function toBlob(url, {
1210
+ type = "svg",
1211
+ scale = 1,
1212
+ backgroundColor = "#fff",
1213
+ quality
1214
+ } = {}) {
1215
+ const mime = {
1216
+ jpg: "image/jpeg",
1217
+ jpeg: "image/jpeg",
1218
+ png: "image/png",
1219
+ webp: "image/webp"
1220
+ }[type] || "image/png";
1221
+ if (type === "svg") {
1222
+ const svgText = decodeURIComponent(url.split(",")[1]);
1223
+ return new Blob([svgText], { type: "image/svg+xml" });
1224
+ }
1225
+ const canvas = await createBackground(url, { dpr: 1, scale }, backgroundColor);
1226
+ return new Promise((resolve) => {
1227
+ canvas.toBlob((blob) => resolve(blob), `${mime}`, quality);
1228
+ });
1007
1229
  }
1008
1230
  async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
1009
1231
  const baseCanvas = await toCanvas(url, { dpr, scale });
@@ -1055,6 +1277,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
1055
1277
  async function snapdom(element, options = {}) {
1056
1278
  options = { scale: 1, ...options };
1057
1279
  if (!element) throw new Error("Element cannot be null or undefined");
1280
+ if (options.iconFonts) {
1281
+ extendIconFonts(options.iconFonts);
1282
+ }
1058
1283
  return await snapdom.capture(element, options);
1059
1284
  }
1060
1285
  snapdom.capture = async (el, options = {}) => {
@@ -1067,7 +1292,7 @@ snapdom.capture = async (el, options = {}) => {
1067
1292
  toRaw: () => url,
1068
1293
  toImg: () => toImg(url, { dpr, scale }),
1069
1294
  toCanvas: () => toCanvas(url, { dpr, scale }),
1070
- toBlob: () => toBlob(url),
1295
+ toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
1071
1296
  toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
1072
1297
  toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
1073
1298
  toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
@@ -1077,7 +1302,7 @@ snapdom.capture = async (el, options = {}) => {
1077
1302
  snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
1078
1303
  snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
1079
1304
  snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
1080
- snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
1305
+ snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
1081
1306
  snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
1082
1307
  snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
1083
1308
  snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
@@ -1094,7 +1319,7 @@ snapdom.download = async (el, options = {}) => {
1094
1319
 
1095
1320
  // src/api/preCache.js
1096
1321
  async function preCache(root = document, options = {}) {
1097
- const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
1322
+ const { embedFonts = true, reset = false } = options;
1098
1323
  if (reset) {
1099
1324
  imageCache.clear();
1100
1325
  bgCache.clear();
@@ -1113,9 +1338,8 @@ async function preCache(root = document, options = {}) {
1113
1338
  for (const img of imgEls) {
1114
1339
  const src = img.src;
1115
1340
  if (!imageCache.has(src)) {
1116
- const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
1117
1341
  promises.push(
1118
- fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
1342
+ fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
1119
1343
  })
1120
1344
  );
1121
1345
  }
@@ -1128,7 +1352,7 @@ async function preCache(root = document, options = {}) {
1128
1352
  const isUrl = entry.startsWith("url(");
1129
1353
  if (isUrl) {
1130
1354
  promises.push(
1131
- inlineSingleBackgroundEntry(entry, { crossOrigin: crossOriginFn, skipInline: true }).catch(() => {
1355
+ inlineSingleBackgroundEntry(entry, options).catch(() => {
1132
1356
  })
1133
1357
  );
1134
1358
  }
@@ -1136,7 +1360,7 @@ async function preCache(root = document, options = {}) {
1136
1360
  }
1137
1361
  }
1138
1362
  if (embedFonts) {
1139
- await embedCustomFonts({ ignoreIconFonts: !embedFonts, preCached: true });
1363
+ await embedCustomFonts({ preCached: true });
1140
1364
  }
1141
1365
  await Promise.all(promises);
1142
1366
  }