@zumer/snapdom 1.9.7 → 1.9.9

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.9.7
4
+ * v.1.9.9
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -18,18 +18,10 @@ var cache = {
18
18
  font: /* @__PURE__ */ new Set(),
19
19
  snapshot: /* @__PURE__ */ new WeakMap(),
20
20
  snapshotKey: /* @__PURE__ */ new Map(),
21
- preStyleMap: /* @__PURE__ */ new Map(),
22
- preStyle: /* @__PURE__ */ new WeakMap(),
23
- preNodeMap: /* @__PURE__ */ new Map(),
24
21
  reset: resetCache
25
22
  };
26
23
  function resetCache() {
27
24
  cache.computedStyle = /* @__PURE__ */ new WeakMap();
28
- cache.snapshot = /* @__PURE__ */ new WeakMap();
29
- cache.snapshotKey.clear();
30
- cache.preStyleMap.clear();
31
- cache.preStyle = /* @__PURE__ */ new WeakMap();
32
- cache.preNodeMap.clear();
33
25
  }
34
26
 
35
27
  // src/utils/cssTools.js
@@ -81,7 +73,7 @@ function getDefaultStyleForTag(tagName) {
81
73
  if (cache.defaultStyle.has(tagName)) {
82
74
  return cache.defaultStyle.get(tagName);
83
75
  }
84
- const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
76
+ const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
85
77
  if (skipTags.has(tagName)) {
86
78
  const empty = {};
87
79
  cache.defaultStyle.set(tagName, empty);
@@ -111,10 +103,14 @@ function getDefaultStyleForTag(tagName) {
111
103
  cache.defaultStyle.set(tagName, defaults);
112
104
  return defaults;
113
105
  }
106
+ var IGNORED_PROPS = /* @__PURE__ */ new Set([
107
+ "-webkit-locale"
108
+ ]);
114
109
  function getStyleKey(snapshot, tagName, compress = false) {
115
110
  const entries = [];
116
111
  const defaultStyles = getDefaultStyleForTag(tagName);
117
112
  for (let [prop, value] of Object.entries(snapshot)) {
113
+ if (IGNORED_PROPS.has(prop)) continue;
118
114
  if (!compress) {
119
115
  if (value) {
120
116
  entries.push(`${prop}:${value}`);
@@ -159,11 +155,12 @@ function generateDedupedBaseCSS(usedTagNames) {
159
155
  }
160
156
  return css;
161
157
  }
162
- function generateCSSClasses() {
163
- const keySet = new Set(cache.preStyleMap.values());
158
+ function generateCSSClasses(styleMap) {
159
+ const keySet = new Set(styleMap.values());
164
160
  const classMap = /* @__PURE__ */ new Map();
165
161
  let counter = 1;
166
162
  for (const key of keySet) {
163
+ if (!key.trim()) continue;
167
164
  classMap.set(key, `c${counter++}`);
168
165
  }
169
166
  return classMap;
@@ -229,7 +226,25 @@ function extractURL(value) {
229
226
  if (url.startsWith("#")) return null;
230
227
  return url;
231
228
  }
232
- function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
229
+ async function fetchResource(url, { useProxy = "" } = {}) {
230
+ async function doFetch(u) {
231
+ const res = await fetch(u);
232
+ if (!res.ok) throw new Error(`[snapdom] Failed to fetch resource: ${u}`);
233
+ return res;
234
+ }
235
+ try {
236
+ return await doFetch(url);
237
+ } catch (e) {
238
+ if (useProxy && typeof useProxy === "string") {
239
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
240
+ return doFetch(proxied);
241
+ }
242
+ throw e;
243
+ }
244
+ }
245
+ var _inflight = /* @__PURE__ */ new Map();
246
+ var _errorCache = /* @__PURE__ */ new Map();
247
+ function fetchImage(src, { timeout = 3e3, useProxy = "", errorTTL = 8e3 } = {}) {
233
248
  function getCrossOriginMode(url) {
234
249
  try {
235
250
  const parsed = new URL(url, window.location.href);
@@ -238,105 +253,157 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
238
253
  return "anonymous";
239
254
  }
240
255
  }
241
- async function fetchWithFallback(url) {
242
- const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
243
- mode: "cors",
244
- credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
245
- }).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
246
- const reader = new FileReader();
247
- reader.onloadend = () => {
248
- const base64 = reader.result;
249
- if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
250
- reject(new Error("Invalid image data URL"));
251
- return;
252
- }
253
- resolve(base64);
254
- };
255
- reader.onerror = () => reject(new Error("FileReader error"));
256
- reader.readAsDataURL(blob);
257
- }));
256
+ const ok = (data) => ({ ok: true, data });
257
+ const fail = (e) => ({ ok: false, error: e instanceof Error ? e : new Error(String(e)) });
258
+ function fetchBlobAsDataURLSafe(fetchUrl) {
258
259
  try {
259
- return await fetchBlobAsDataURL(url);
260
+ return fetch(fetchUrl, {
261
+ mode: "cors",
262
+ credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
263
+ }).then((r) => {
264
+ if (!r.ok) return fail(new Error("HTTP " + r.status));
265
+ return r.blob().then((blob) => new Promise((resolve) => {
266
+ const reader = new FileReader();
267
+ reader.onloadend = () => {
268
+ const base64 = reader.result;
269
+ if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
270
+ resolve(fail(new Error("Invalid image data URL")));
271
+ } else {
272
+ resolve(ok(base64));
273
+ }
274
+ };
275
+ reader.onerror = () => resolve(fail(new Error("FileReader error")));
276
+ reader.readAsDataURL(blob);
277
+ }));
278
+ }).catch((e) => fail(e));
260
279
  } catch (e) {
280
+ return Promise.resolve(fail(e));
281
+ }
282
+ }
283
+ function fetchWithFallbackOnceSafe(url) {
284
+ return fetchBlobAsDataURLSafe(url).then((r) => {
285
+ if (r.ok) return r;
261
286
  if (useProxy && typeof useProxy === "string") {
262
287
  const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
263
- try {
264
- return await fetchBlobAsDataURL(proxied);
265
- } catch {
266
- throw new Error("[SnapDOM - fetchImage] CORS restrictions prevented image capture (even via proxy)");
267
- }
268
- } else {
269
- throw new Error("[SnapDOM - fetchImage] Fetch fallback failed and no proxy provided");
288
+ return fetchBlobAsDataURLSafe(proxied).then((r2) => {
289
+ if (r2.ok) return r2;
290
+ return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
291
+ });
270
292
  }
271
- }
293
+ return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
294
+ });
272
295
  }
273
- const crossOriginValue = getCrossOriginMode(src);
274
- if (cache.image.has(src)) {
275
- return Promise.resolve(cache.image.get(src));
296
+ const now = Date.now();
297
+ const until = _errorCache.get(src);
298
+ if (until && until > now) {
299
+ const pr = Promise.reject(new Error("[SnapDOM - fetchImage] Recently failed (cooldown)."));
300
+ pr.catch(() => {
301
+ });
302
+ return pr;
276
303
  }
277
- const isDataURI = src.startsWith("data:image/");
278
- if (isDataURI) {
304
+ if (_inflight.has(src)) return _inflight.get(src);
305
+ const crossOriginValue = getCrossOriginMode(src);
306
+ if (cache.image.has(src)) return Promise.resolve(cache.image.get(src));
307
+ if (src.startsWith("data:image/")) {
279
308
  cache.image.set(src, src);
280
309
  return Promise.resolve(src);
281
310
  }
282
- const isSVG = /\.svg(\?.*)?$/i.test(src);
283
- if (isSVG) {
284
- return (async () => {
285
- try {
286
- const response = await fetch(src, {
287
- mode: "cors",
288
- credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
289
- });
290
- const svgText = await response.text();
291
- const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
292
- cache.image.set(src, encoded);
293
- return encoded;
294
- } catch {
295
- return fetchWithFallback(src);
296
- }
297
- })();
298
- }
299
- return new Promise((resolve, reject) => {
300
- const timeoutId = setTimeout(() => {
301
- reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
302
- }, timeout);
303
- const image = new Image();
304
- image.crossOrigin = crossOriginValue;
305
- image.onload = async () => {
306
- clearTimeout(timeoutId);
307
- try {
308
- await image.decode();
309
- const canvas = document.createElement("canvas");
310
- canvas.width = image.width;
311
- canvas.height = image.height;
312
- const ctx = canvas.getContext("2d");
313
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
314
- const dataURL = canvas.toDataURL("image/png");
315
- cache.image.set(src, dataURL);
316
- resolve(dataURL);
317
- } catch {
311
+ if (/\.svg(\?.*)?$/i.test(src)) {
312
+ const p2 = (async () => {
313
+ const direct = await (async () => {
318
314
  try {
319
- const fallbackDataURL = await fetchWithFallback(src);
320
- cache.image.set(src, fallbackDataURL);
321
- resolve(fallbackDataURL);
315
+ const res = await fetch(src, {
316
+ mode: "cors",
317
+ credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
318
+ });
319
+ if (!res.ok) return fail(new Error("HTTP " + res.status));
320
+ const svgText = await res.text();
321
+ return ok(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`);
322
322
  } catch (e) {
323
- reject(e);
323
+ return fail(e);
324
324
  }
325
+ })();
326
+ if (direct.ok) {
327
+ cache.image.set(src, direct.data);
328
+ return direct.data;
325
329
  }
326
- };
327
- image.onerror = async () => {
328
- clearTimeout(timeoutId);
329
- console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
330
- try {
331
- const fallbackDataURL = await fetchWithFallback(src);
332
- cache.image.set(src, fallbackDataURL);
333
- resolve(fallbackDataURL);
334
- } catch (e) {
335
- reject(e);
330
+ const via = await fetchWithFallbackOnceSafe(src);
331
+ if (via.ok) {
332
+ cache.image.set(src, via.data);
333
+ return via.data;
336
334
  }
335
+ _errorCache.set(src, now + errorTTL);
336
+ return Promise.reject(via.error);
337
+ })();
338
+ _inflight.set(src, p2);
339
+ p2.finally(() => _inflight.delete(src));
340
+ p2.catch(() => {
341
+ });
342
+ return p2;
343
+ }
344
+ const p = new Promise((resolve, reject) => {
345
+ let finished = false;
346
+ const img = new Image();
347
+ const finish = (fn) => (arg) => {
348
+ if (finished) return;
349
+ finished = true;
350
+ clearTimeout(timeoutId);
351
+ img.onload = img.onerror = null;
352
+ fn(arg);
337
353
  };
338
- image.src = src;
354
+ const onSuccess = (d) => {
355
+ cache.image.set(src, d);
356
+ resolve(d);
357
+ };
358
+ const onFinalError = (e) => {
359
+ _errorCache.set(src, Date.now() + errorTTL);
360
+ reject(e);
361
+ };
362
+ const timeoutId = setTimeout(
363
+ finish(() => {
364
+ fetchWithFallbackOnceSafe(src).then((r) => {
365
+ if (r.ok) onSuccess(r.data);
366
+ else onFinalError(new Error("Image load timed out"));
367
+ });
368
+ }),
369
+ timeout
370
+ );
371
+ img.crossOrigin = crossOriginValue;
372
+ img.onload = finish(() => {
373
+ Promise.resolve(img.decode()).then(() => {
374
+ try {
375
+ const canvas = document.createElement("canvas");
376
+ canvas.width = img.naturalWidth || img.width;
377
+ canvas.height = img.naturalHeight || img.height;
378
+ const ctx = canvas.getContext("2d");
379
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
380
+ onSuccess(canvas.toDataURL("image/png"));
381
+ } catch {
382
+ fetchWithFallbackOnceSafe(src).then((r) => {
383
+ if (r.ok) onSuccess(r.data);
384
+ else onFinalError(r.error);
385
+ });
386
+ }
387
+ }).catch(() => {
388
+ fetchWithFallbackOnceSafe(src).then((r) => {
389
+ if (r.ok) onSuccess(r.data);
390
+ else onFinalError(r.error);
391
+ });
392
+ });
393
+ });
394
+ img.onerror = finish(() => {
395
+ fetchWithFallbackOnceSafe(src).then((r) => {
396
+ if (r.ok) onSuccess(r.data);
397
+ else onFinalError(r.error);
398
+ });
399
+ });
400
+ img.src = src;
401
+ });
402
+ _inflight.set(src, p);
403
+ p.finally(() => _inflight.delete(src));
404
+ p.catch(() => {
339
405
  });
406
+ return p;
340
407
  }
341
408
  function snapshotComputedStyle(style) {
342
409
  const snap = {};
@@ -393,8 +460,11 @@ function splitBackgroundImage(bg) {
393
460
  }
394
461
 
395
462
  // src/modules/styles.js
463
+ var snapshotCache = /* @__PURE__ */ new WeakMap();
464
+ var snapshotKeyCache = /* @__PURE__ */ new Map();
396
465
  function snapshotComputedStyleFull(style) {
397
466
  const result = {};
467
+ const computedVisibility = style.getPropertyValue("visibility");
398
468
  for (let i = 0; i < style.length; i++) {
399
469
  const prop = style[i];
400
470
  let val = style.getPropertyValue(prop);
@@ -403,32 +473,47 @@ function snapshotComputedStyleFull(style) {
403
473
  }
404
474
  result[prop] = val;
405
475
  }
476
+ if (computedVisibility === "hidden") {
477
+ result.opacity = "0";
478
+ }
406
479
  return result;
407
480
  }
408
- function inlineAllStyles(source, clone, compress) {
481
+ function inlineAllStyles(source, clone, styleMap, cache2, compress) {
409
482
  if (source.tagName === "STYLE") return;
410
- if (!cache.preStyle.has(source)) {
411
- cache.preStyle.set(source, getStyle(source));
483
+ if (!cache2.has(source)) {
484
+ cache2.set(source, getStyle(source));
412
485
  }
413
- const style = cache.preStyle.get(source);
414
- if (!cache.snapshot.has(source)) {
486
+ const style = cache2.get(source);
487
+ if (!snapshotCache.has(source)) {
415
488
  const snapshot2 = snapshotComputedStyleFull(style);
416
- cache.snapshot.set(source, snapshot2);
489
+ snapshotCache.set(source, snapshot2);
417
490
  }
418
- const snapshot = cache.snapshot.get(source);
491
+ const snapshot = snapshotCache.get(source);
419
492
  const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
420
- if (cache.snapshotKey.has(hash)) {
421
- cache.preStyleMap.set(clone, cache.snapshotKey.get(hash));
493
+ if (snapshotKeyCache.has(hash)) {
494
+ styleMap.set(clone, snapshotKeyCache.get(hash));
422
495
  return;
423
496
  }
424
497
  const tagName = source.tagName?.toLowerCase() || "div";
425
498
  const key = getStyleKey(snapshot, tagName, compress);
426
- cache.snapshotKey.set(hash, key);
427
- cache.preStyleMap.set(clone, key);
499
+ snapshotKeyCache.set(hash, key);
500
+ styleMap.set(clone, key);
428
501
  }
429
502
 
430
503
  // src/core/clone.js
431
- function deepClone(node, compress, options = {}, originalRoot) {
504
+ function freezeImgSrcset(original, cloned) {
505
+ try {
506
+ const chosen = original.currentSrc || original.src || "";
507
+ if (!chosen) return;
508
+ cloned.setAttribute("src", chosen);
509
+ cloned.removeAttribute("srcset");
510
+ cloned.removeAttribute("sizes");
511
+ cloned.loading = "eager";
512
+ cloned.decoding = "sync";
513
+ } catch {
514
+ }
515
+ }
516
+ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
432
517
  if (!node) throw new Error("Invalid node");
433
518
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
434
519
  let pendingSelectValue = null;
@@ -477,8 +562,8 @@ function deepClone(node, compress, options = {}, originalRoot) {
477
562
  }
478
563
  if (node.getAttribute("data-capture") === "placeholder") {
479
564
  const clone2 = node.cloneNode(false);
480
- cache.preNodeMap.set(clone2, node);
481
- inlineAllStyles(node, clone2, compress);
565
+ nodeMap.set(clone2, node);
566
+ inlineAllStyles(node, clone2, styleMap, styleCache, compress);
482
567
  const placeholder = document.createElement("div");
483
568
  placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
484
569
  placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
@@ -491,14 +576,17 @@ function deepClone(node, compress, options = {}, originalRoot) {
491
576
  img.src = dataURL;
492
577
  img.width = node.width;
493
578
  img.height = node.height;
494
- cache.preNodeMap.set(img, node);
495
- inlineAllStyles(node, img, compress);
579
+ nodeMap.set(img, node);
580
+ inlineAllStyles(node, img, styleMap, styleCache, compress);
496
581
  return img;
497
582
  }
498
583
  let clone;
499
584
  try {
500
585
  clone = node.cloneNode(false);
501
- cache.preNodeMap.set(clone, node);
586
+ nodeMap.set(clone, node);
587
+ if (node.tagName === "IMG") {
588
+ freezeImgSrcset(node, clone);
589
+ }
502
590
  } catch (err) {
503
591
  console.error("[Snapdom] Failed to clone node:", node, err);
504
592
  throw err;
@@ -523,7 +611,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
523
611
  if (node instanceof HTMLSelectElement) {
524
612
  pendingSelectValue = node.value;
525
613
  }
526
- inlineAllStyles(node, clone, compress);
614
+ inlineAllStyles(node, clone, styleMap, styleCache, compress);
527
615
  if (node.shadowRoot) {
528
616
  const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
529
617
  if (hasSlot) {
@@ -531,8 +619,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
531
619
  if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
532
620
  const cssText = child.textContent || "";
533
621
  if (cssText.trim() && compress) {
534
- if (!cache.preStyle) cache.preStyle = /* @__PURE__ */ new WeakMap();
535
- cache.preStyle.set(child, cssText);
622
+ styleCache.set(child, cssText);
536
623
  }
537
624
  }
538
625
  }
@@ -542,12 +629,11 @@ function deepClone(node, compress, options = {}, originalRoot) {
542
629
  if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
543
630
  const cssText = child.textContent || "";
544
631
  if (cssText.trim() && compress) {
545
- if (!cache.preStyle) cache.preStyle = /* @__PURE__ */ new WeakMap();
546
- cache.preStyle.set(child, cssText);
632
+ styleCache.set(child, cssText);
547
633
  }
548
634
  continue;
549
635
  }
550
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
636
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
551
637
  if (clonedChild) shadowFrag.appendChild(clonedChild);
552
638
  }
553
639
  clone.appendChild(shadowFrag);
@@ -558,14 +644,14 @@ function deepClone(node, compress, options = {}, originalRoot) {
558
644
  const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
559
645
  const fragment = document.createDocumentFragment();
560
646
  for (const child of nodesToClone) {
561
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
647
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
562
648
  if (clonedChild) fragment.appendChild(clonedChild);
563
649
  }
564
650
  return fragment;
565
651
  }
566
652
  for (const child of node.childNodes) {
567
653
  if (clonedAssignedNodes.has(child)) continue;
568
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
654
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
569
655
  if (clonedChild) clone.appendChild(clonedChild);
570
656
  }
571
657
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
@@ -622,25 +708,38 @@ function isIconFont(input) {
622
708
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
623
709
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
624
710
  const dpr = window.devicePixelRatio || 1;
625
- const tempCanvas = document.createElement("canvas");
626
- const tempCtx = tempCanvas.getContext("2d");
627
- tempCtx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
628
- const metrics = tempCtx.measureText(unicodeChar);
629
- const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
630
- const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
631
- const height = ascent + descent;
632
- const width = metrics.width;
711
+ await document.fonts.ready;
712
+ const span = document.createElement("span");
713
+ span.textContent = unicodeChar;
714
+ span.style.position = "absolute";
715
+ span.style.visibility = "hidden";
716
+ span.style.fontFamily = `"${fontFamily}"`;
717
+ span.style.fontWeight = fontWeight || "normal";
718
+ span.style.fontSize = `${fontSize}px`;
719
+ span.style.lineHeight = "1";
720
+ span.style.whiteSpace = "nowrap";
721
+ span.style.padding = "0";
722
+ span.style.margin = "0";
723
+ document.body.appendChild(span);
724
+ const rect = span.getBoundingClientRect();
725
+ const width = Math.ceil(rect.width);
726
+ const height = Math.ceil(rect.height);
727
+ document.body.removeChild(span);
633
728
  const canvas = document.createElement("canvas");
634
- canvas.width = Math.ceil(width * dpr);
635
- canvas.height = Math.ceil(height * dpr);
729
+ canvas.width = width * dpr;
730
+ canvas.height = height * dpr;
636
731
  const ctx = canvas.getContext("2d");
637
732
  ctx.scale(dpr, dpr);
638
- ctx.font = tempCtx.font;
733
+ ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
639
734
  ctx.textAlign = "left";
640
- ctx.textBaseline = "alphabetic";
735
+ ctx.textBaseline = "top";
641
736
  ctx.fillStyle = color;
642
- ctx.fillText(unicodeChar, 0, ascent);
643
- return canvas.toDataURL();
737
+ ctx.fillText(unicodeChar, 0, 0);
738
+ return {
739
+ dataUrl: canvas.toDataURL(),
740
+ width,
741
+ height
742
+ };
644
743
  }
645
744
  function isStylesheetLoaded(href) {
646
745
  return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
@@ -657,7 +756,7 @@ function injectLinkIfMissing(href) {
657
756
  document.head.appendChild(link);
658
757
  });
659
758
  }
660
- async function embedCustomFonts({ preCached = false } = {}) {
759
+ async function embedCustomFonts({ preCached = false, localFonts = [], useProxy = "" } = {}) {
661
760
  if (cache.resource.has("fonts-embed-css")) {
662
761
  if (preCached) {
663
762
  const style = document.createElement("style");
@@ -667,6 +766,15 @@ async function embedCustomFonts({ preCached = false } = {}) {
667
766
  }
668
767
  return cache.resource.get("fonts-embed-css");
669
768
  }
769
+ const loadedFonts = /* @__PURE__ */ new Set();
770
+ try {
771
+ for (const f of document.fonts) {
772
+ if (f.status === "loaded") {
773
+ loadedFonts.add(`${f.family}__${f.weight || "normal"}__${f.style || "normal"}`);
774
+ }
775
+ }
776
+ } catch {
777
+ }
670
778
  const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
671
779
  const styleImports = [];
672
780
  for (const styleTag of document.querySelectorAll("style")) {
@@ -685,26 +793,48 @@ async function embedCustomFonts({ preCached = false } = {}) {
685
793
  let finalCSS = "";
686
794
  for (const link of links) {
687
795
  try {
688
- const res = await fetch(link.href);
796
+ const res = await fetchResource(link.href, { useProxy });
689
797
  const cssText = await res.text();
690
798
  if (isIconFont(link.href) || isIconFont(cssText)) continue;
691
- const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
692
- const inlinedCSS = await Promise.all(
693
- Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
799
+ const faceRegex = /@font-face[^{}]*{[^}]*}/g;
800
+ let cssFinal = cssText;
801
+ for (const face of cssText.match(faceRegex) || []) {
802
+ const famMatch = face.match(/font-family:\s*([^;]+);/i);
803
+ if (!famMatch) continue;
804
+ const family = famMatch[1].replace(/['"]/g, "").trim();
805
+ const weightMatch = face.match(/font-weight:\s*([^;]+);/i);
806
+ const styleMatch = face.match(/font-style:\s*([^;]+);/i);
807
+ const weight = weightMatch ? weightMatch[1].trim() : "normal";
808
+ const style = styleMatch ? styleMatch[1].trim() : "normal";
809
+ const key = `${family}__${weight}__${style}`;
810
+ const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
811
+ const hasURL = /url\(/i.test(face);
812
+ const hasLocal = /local\(/i.test(face);
813
+ if (!hasURL && hasLocal) {
814
+ continue;
815
+ }
816
+ if (!loadedFonts.has(key)) {
817
+ cssFinal = cssFinal.replace(face, "");
818
+ continue;
819
+ }
820
+ let inlined = face;
821
+ const matches = Array.from(face.matchAll(urlRegex));
822
+ for (const match of matches) {
694
823
  let rawUrl = extractURL(match[0]);
695
- if (!rawUrl) return null;
824
+ if (!rawUrl) continue;
696
825
  let url = rawUrl;
697
826
  if (!url.startsWith("http") && !url.startsWith("data:")) {
698
827
  url = new URL(url, link.href).href;
699
828
  }
700
- if (isIconFont(url)) return null;
829
+ if (isIconFont(url)) continue;
701
830
  if (cache.resource.has(url)) {
702
831
  cache.font.add(url);
703
- return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
832
+ inlined = inlined.replace(match[0], `url(${cache.resource.get(url)})`);
833
+ continue;
704
834
  }
705
- if (cache.font.has(url)) return null;
835
+ if (cache.font.has(url)) continue;
706
836
  try {
707
- const fontRes = await fetch(url);
837
+ const fontRes = await fetchResource(url, { useProxy });
708
838
  const blob = await fontRes.blob();
709
839
  const b64 = await new Promise((resolve) => {
710
840
  const reader = new FileReader();
@@ -713,16 +843,12 @@ async function embedCustomFonts({ preCached = false } = {}) {
713
843
  });
714
844
  cache.resource.set(url, b64);
715
845
  cache.font.add(url);
716
- return { original: match[0], inlined: `url(${b64})` };
846
+ inlined = inlined.replace(match[0], `url(${b64})`);
717
847
  } catch (e) {
718
848
  console.warn("[snapdom] Failed to fetch font resource:", url);
719
- return null;
720
849
  }
721
- })
722
- );
723
- let cssFinal = cssText;
724
- for (const r of inlinedCSS) {
725
- if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
850
+ }
851
+ cssFinal = cssFinal.replace(face, inlined);
726
852
  }
727
853
  finalCSS += cssFinal + "\n";
728
854
  } catch (e) {
@@ -737,14 +863,18 @@ async function embedCustomFonts({ preCached = false } = {}) {
737
863
  const src = rule.style.getPropertyValue("src");
738
864
  const family = rule.style.getPropertyValue("font-family");
739
865
  if (!src || isIconFont(family)) continue;
866
+ const weightVal = rule.style.getPropertyValue("font-weight") || "normal";
867
+ const styleVal = rule.style.getPropertyValue("font-style") || "normal";
868
+ const key = `${family}__${weightVal}__${styleVal}`;
740
869
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
741
870
  const localRegex = /local\((["']?)[^)]+?\1\)/g;
742
871
  const hasURL = !!src.match(urlRegex);
743
872
  const hasLocal = !!src.match(localRegex);
744
873
  if (!hasURL && hasLocal) {
745
- finalCSS += `@font-face{font-family:${family};src:${src};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
874
+ finalCSS += `@font-face{font-family:${family};src:${src};font-style:${styleVal};font-weight:${weightVal};}`;
746
875
  continue;
747
876
  }
877
+ if (!loadedFonts.has(key)) continue;
748
878
  let inlinedSrc = src;
749
879
  const matches = Array.from(src.matchAll(urlRegex));
750
880
  for (const match of matches) {
@@ -762,7 +892,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
762
892
  }
763
893
  if (cache.font.has(url)) continue;
764
894
  try {
765
- const res = await fetch(url);
895
+ const res = await fetchResource(url, { useProxy });
766
896
  const blob = await res.blob();
767
897
  const b64 = await new Promise((resolve) => {
768
898
  const reader = new FileReader();
@@ -776,7 +906,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
776
906
  console.warn("[snapdom] Failed to fetch font URL:", url);
777
907
  }
778
908
  }
779
- finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
909
+ finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleVal};font-weight:${weightVal};}`;
780
910
  }
781
911
  }
782
912
  }
@@ -794,7 +924,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
794
924
  cache.font.add(font._snapdomSrc);
795
925
  } else if (!cache.font.has(font._snapdomSrc)) {
796
926
  try {
797
- const res = await fetch(font._snapdomSrc);
927
+ const res = await fetchResource(font._snapdomSrc, { useProxy });
798
928
  const blob = await res.blob();
799
929
  b64 = await new Promise((resolve) => {
800
930
  const reader = new FileReader();
@@ -812,6 +942,32 @@ async function embedCustomFonts({ preCached = false } = {}) {
812
942
  finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
813
943
  }
814
944
  }
945
+ for (const font of localFonts) {
946
+ if (!font || typeof font !== "object") continue;
947
+ const { family, src, weight = "normal", style = "normal" } = font;
948
+ if (!family || !src) continue;
949
+ let b64 = src;
950
+ if (!b64.startsWith("data:")) {
951
+ try {
952
+ const res = await fetchResource(src, { useProxy });
953
+ const blob = await res.blob();
954
+ b64 = await new Promise((resolve) => {
955
+ const reader = new FileReader();
956
+ reader.onload = () => resolve(reader.result);
957
+ reader.readAsDataURL(blob);
958
+ });
959
+ cache.resource.set(src, b64);
960
+ cache.font.add(src);
961
+ } catch (e) {
962
+ console.warn("[snapdom] Failed to load local font:", src);
963
+ continue;
964
+ }
965
+ } else {
966
+ cache.resource.set(src, b64);
967
+ cache.font.add(src);
968
+ }
969
+ finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};}`;
970
+ }
815
971
  if (finalCSS) {
816
972
  cache.resource.set("fonts-embed-css", finalCSS);
817
973
  if (preCached) {
@@ -825,18 +981,20 @@ async function embedCustomFonts({ preCached = false } = {}) {
825
981
  }
826
982
 
827
983
  // src/modules/pseudo.js
828
- async function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
984
+ async function inlinePseudoElements(source, clone, styleMap, styleCache, options) {
829
985
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
830
986
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
831
987
  try {
832
988
  const style = getStyle(source, pseudo);
833
989
  if (!style || typeof style[Symbol.iterator] !== "function") continue;
990
+ const isEmptyPseudo = style.content === "none" && style.backgroundImage === "none" && style.backgroundColor === "transparent" && (style.borderStyle === "none" || parseFloat(style.borderWidth) === 0) && (!style.transform || style.transform === "none") && style.display === "inline";
991
+ if (isEmptyPseudo) continue;
834
992
  if (pseudo === "::first-letter") {
835
993
  const normal = getComputedStyle(source);
836
994
  const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
837
995
  if (!isMeaningful) continue;
838
996
  const textNode = Array.from(clone.childNodes).find(
839
- (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
997
+ (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim().length > 0
840
998
  );
841
999
  if (!textNode) continue;
842
1000
  const text = textNode.textContent;
@@ -848,57 +1006,55 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
848
1006
  span.textContent = first;
849
1007
  span.dataset.snapdomPseudo = "::first-letter";
850
1008
  const snapshot2 = snapshotComputedStyle(style);
851
- const key2 = getStyleKey(snapshot2, "span", compress);
852
- cache.preStyleMap.set(span, key2);
1009
+ const key2 = getStyleKey(snapshot2, "span", options);
1010
+ styleMap.set(span, key2);
853
1011
  const restNode = document.createTextNode(rest);
854
1012
  clone.replaceChild(restNode, textNode);
855
1013
  clone.insertBefore(span, restNode);
856
1014
  continue;
857
1015
  }
858
- const content = style.getPropertyValue("content");
859
- const bg = style.getPropertyValue("background-image");
860
- const bgColor = style.getPropertyValue("background-color");
861
- const fontFamily = style.getPropertyValue("font-family");
862
- const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
863
- const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
864
- const color = style.getPropertyValue("color") || "#000";
865
- const display = style.getPropertyValue("display");
866
- const width = parseFloat(style.getPropertyValue("width"));
867
- const height = parseFloat(style.getPropertyValue("height"));
868
- const borderStyle = style.getPropertyValue("border-style");
869
- const transform = style.getPropertyValue("transform");
1016
+ const content = style.content;
1017
+ const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
1018
+ const bg = style.backgroundImage;
1019
+ const bgColor = style.backgroundColor;
1020
+ const fontFamily = style.fontFamily;
1021
+ const fontSize = parseInt(style.fontSize) || 32;
1022
+ const fontWeight = parseInt(style.fontWeight) || false;
1023
+ const color = style.color || "#000";
1024
+ const display = style.display;
1025
+ const width = parseFloat(style.width);
1026
+ const height = parseFloat(style.height);
1027
+ const borderStyle = style.borderStyle;
1028
+ const borderWidth = parseFloat(style.borderWidth);
1029
+ const transform = style.transform;
870
1030
  const isIconFont2 = isIconFont(fontFamily);
871
- let cleanContent;
872
- if (/counter\s*\(|counters\s*\(/.test(content)) {
873
- cleanContent = "- ";
874
- } else {
875
- cleanContent = parseContent(content);
876
- }
877
- const hasContent = content !== "none";
878
- const hasExplicitContent = hasContent && cleanContent !== "";
1031
+ const hasExplicitContent = content !== "none" && cleanContent !== "";
879
1032
  const hasBg = bg && bg !== "none";
880
1033
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
881
1034
  const hasBox = display !== "inline" && (width > 0 || height > 0);
882
- const hasBorder = borderStyle && borderStyle !== "none";
1035
+ const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
883
1036
  const hasTransform = transform && transform !== "none";
884
1037
  const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
885
1038
  if (!shouldRender) continue;
886
1039
  const pseudoEl = document.createElement("span");
887
1040
  pseudoEl.dataset.snapdomPseudo = pseudo;
1041
+ pseudoEl.style.verticalAlign = "middle";
888
1042
  const snapshot = snapshotComputedStyle(style);
889
- const key = getStyleKey(snapshot, "span", compress);
890
- cache.preStyleMap.set(pseudoEl, key);
1043
+ const key = getStyleKey(snapshot, "span", options);
1044
+ styleMap.set(pseudoEl, key);
891
1045
  if (isIconFont2 && cleanContent.length === 1) {
1046
+ const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
892
1047
  const imgEl = document.createElement("img");
893
- imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
894
- imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
1048
+ imgEl.src = dataUrl;
1049
+ imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
895
1050
  pseudoEl.appendChild(imgEl);
1051
+ clone.dataset.snapdomHasIcon = "true";
896
1052
  } else if (cleanContent.startsWith("url(")) {
897
1053
  const rawUrl = extractURL(cleanContent);
898
- if (rawUrl && rawUrl.trim() !== "") {
1054
+ if (rawUrl?.trim()) {
899
1055
  try {
900
1056
  const imgEl = document.createElement("img");
901
- const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
1057
+ const dataUrl = await fetchImage(safeEncodeURI(rawUrl), options);
902
1058
  imgEl.src = dataUrl;
903
1059
  imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
904
1060
  pseudoEl.appendChild(imgEl);
@@ -912,19 +1068,21 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
912
1068
  if (hasBg) {
913
1069
  try {
914
1070
  const bgSplits = splitBackgroundImage(bg);
915
- const newBgParts = await Promise.all(
916
- bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
917
- );
1071
+ const newBgParts = await Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
918
1072
  pseudoEl.style.backgroundImage = newBgParts.join(", ");
919
1073
  } catch (e) {
920
1074
  console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
921
1075
  }
922
1076
  }
923
1077
  if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
924
- const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
1078
+ const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
925
1079
  const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
926
1080
  if (!hasVisibleBox) continue;
927
- pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
1081
+ if (pseudo === "::before") {
1082
+ clone.insertBefore(pseudoEl, clone.firstChild);
1083
+ } else {
1084
+ clone.appendChild(pseudoEl);
1085
+ }
928
1086
  } catch (e) {
929
1087
  console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
930
1088
  }
@@ -932,84 +1090,111 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
932
1090
  const sChildren = Array.from(source.children);
933
1091
  const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
934
1092
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
935
- await inlinePseudoElements(
936
- sChildren[i],
937
- cChildren[i],
938
- compress,
939
- embedFonts,
940
- useProxy
941
- );
1093
+ await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, options);
942
1094
  }
943
1095
  }
944
1096
 
945
1097
  // src/modules/svgDefs.js
946
- function inlineExternalDef(root) {
947
- if (!root) return;
948
- const defsSources = document.querySelectorAll("svg > defs");
949
- if (!defsSources.length) return;
950
- root.querySelectorAll("svg").forEach((svg) => {
951
- const uses = svg.querySelectorAll("use");
952
- if (!uses.length) return;
953
- const usedIds = /* @__PURE__ */ new Set();
954
- uses.forEach((use) => {
955
- const href = use.getAttribute("xlink:href") || use.getAttribute("href");
956
- if (href && href.startsWith("#")) usedIds.add(href.slice(1));
957
- });
958
- if (!usedIds.size) return;
959
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
960
- for (const id of usedIds) {
961
- for (const source of defsSources) {
962
- const def = source.querySelector(`#${CSS.escape(id)}`);
963
- if (def) {
964
- defs.appendChild(def.cloneNode(true));
965
- break;
1098
+ function inlineExternalDefsAndSymbols(rootElement) {
1099
+ if (!rootElement) return;
1100
+ const usedIds = /* @__PURE__ */ new Set();
1101
+ rootElement.querySelectorAll("use").forEach((use) => {
1102
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
1103
+ if (href && href.startsWith("#")) {
1104
+ usedIds.add(href.slice(1));
1105
+ }
1106
+ });
1107
+ if (!usedIds.size) return;
1108
+ const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
1109
+ const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
1110
+ const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
1111
+ let container = rootElement.querySelector("svg.inline-defs-container");
1112
+ if (!container) {
1113
+ container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1114
+ container.setAttribute("aria-hidden", "true");
1115
+ container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
1116
+ container.classList.add("inline-defs-container");
1117
+ rootElement.insertBefore(container, rootElement.firstChild);
1118
+ }
1119
+ const existingIds = /* @__PURE__ */ new Set();
1120
+ rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
1121
+ existingIds.add(el.id);
1122
+ });
1123
+ usedIds.forEach((id) => {
1124
+ if (existingIds.has(id)) return;
1125
+ const symbol = globalSymbols.find((sym) => sym.id === id);
1126
+ if (symbol) {
1127
+ container.appendChild(symbol.cloneNode(true));
1128
+ existingIds.add(id);
1129
+ return;
1130
+ }
1131
+ for (const defs of globalDefs) {
1132
+ const defEl = defs.querySelector(`#${CSS.escape(id)}`);
1133
+ if (defEl) {
1134
+ let defsContainer = container.querySelector("defs");
1135
+ if (!defsContainer) {
1136
+ defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1137
+ container.appendChild(defsContainer);
966
1138
  }
1139
+ defsContainer.appendChild(defEl.cloneNode(true));
1140
+ existingIds.add(id);
1141
+ break;
967
1142
  }
968
1143
  }
969
- if (defs.childNodes.length) {
970
- svg.insertBefore(defs, svg.firstChild);
971
- }
972
1144
  });
973
1145
  }
974
1146
 
975
1147
  // src/core/prepare.js
976
1148
  async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
1149
+ const styleMap = /* @__PURE__ */ new Map();
1150
+ const styleCache = /* @__PURE__ */ new WeakMap();
1151
+ const nodeMap = /* @__PURE__ */ new Map();
977
1152
  let clone;
978
1153
  let classCSS = "";
1154
+ stabilizeLayout(element);
979
1155
  try {
980
- clone = deepClone(element, compress, options, element);
1156
+ inlineExternalDefsAndSymbols(element);
981
1157
  } catch (e) {
982
- console.warn("deepClone failed:", e);
983
- throw e;
1158
+ console.warn("inlineExternal defs or symbol failed:", e);
984
1159
  }
985
1160
  try {
986
- await inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
1161
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
987
1162
  } catch (e) {
988
- console.warn("inlinePseudoElements failed:", e);
1163
+ console.warn("deepClone failed:", e);
1164
+ throw e;
989
1165
  }
990
1166
  try {
991
- inlineExternalDef(clone);
1167
+ await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
992
1168
  } catch (e) {
993
- console.warn("inlineExternalDef failed:", e);
1169
+ console.warn("inlinePseudoElements failed:", e);
994
1170
  }
1171
+ await resolveBlobUrlsInTree(clone);
995
1172
  if (compress) {
996
- const keyToClass = generateCSSClasses();
1173
+ const keyToClass = generateCSSClasses(styleMap);
997
1174
  classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
998
- for (const [node, key] of cache.preStyleMap.entries()) {
1175
+ for (const [node, key] of styleMap.entries()) {
999
1176
  if (node.tagName === "STYLE") continue;
1177
+ if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
1178
+ node.setAttribute("style", key.replace(/;/g, "; "));
1179
+ continue;
1180
+ }
1000
1181
  const className = keyToClass.get(key);
1001
1182
  if (className) node.classList.add(className);
1002
1183
  const bgImage = node.style?.backgroundImage;
1003
- node.removeAttribute("style");
1184
+ const hasIcon = node.dataset?.snapdomHasIcon;
1004
1185
  if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
1186
+ if (hasIcon) {
1187
+ node.style.verticalAlign = "middle";
1188
+ node.style.display = "inline";
1189
+ }
1005
1190
  }
1006
1191
  } else {
1007
- for (const [node, key] of cache.preStyleMap.entries()) {
1192
+ for (const [node, key] of styleMap.entries()) {
1008
1193
  if (node.tagName === "STYLE") continue;
1009
1194
  node.setAttribute("style", key.replace(/;/g, "; "));
1010
1195
  }
1011
1196
  }
1012
- for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
1197
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
1013
1198
  const scrollX = originalNode.scrollLeft;
1014
1199
  const scrollY = originalNode.scrollTop;
1015
1200
  const hasScroll = scrollX || scrollY;
@@ -1028,9 +1213,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
1028
1213
  cloneNode.appendChild(inner);
1029
1214
  }
1030
1215
  }
1031
- if (element === cache.preNodeMap.get(clone)) {
1032
- const computed = cache.preStyle.get(element) || window.getComputedStyle(element);
1033
- cache.preStyle.set(element, computed);
1216
+ if (element === nodeMap.get(clone)) {
1217
+ const computed = styleCache.get(element) || window.getComputedStyle(element);
1218
+ styleCache.set(element, computed);
1034
1219
  const transform = stripTranslate(computed.transform);
1035
1220
  clone.style.margin = "0";
1036
1221
  clone.style.position = "static";
@@ -1043,19 +1228,160 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
1043
1228
  clone.style.clear = "none";
1044
1229
  clone.style.transform = transform || "";
1045
1230
  }
1046
- for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
1231
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
1047
1232
  if (originalNode.tagName === "PRE") {
1048
1233
  cloneNode.style.marginTop = "0";
1049
1234
  cloneNode.style.marginBlockStart = "0";
1050
1235
  }
1051
1236
  }
1052
- return { clone, classCSS };
1237
+ return { clone, classCSS, styleCache };
1238
+ }
1239
+ function stabilizeLayout(element) {
1240
+ const style = getComputedStyle(element);
1241
+ const outlineStyle = style.outlineStyle;
1242
+ const outlineWidth = style.outlineWidth;
1243
+ const borderStyle = style.borderStyle;
1244
+ const borderWidth = style.borderWidth;
1245
+ const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
1246
+ const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
1247
+ if (outlineVisible && borderAbsent) {
1248
+ element.style.border = `${outlineWidth} solid transparent`;
1249
+ }
1250
+ }
1251
+ var _blobToDataUrlCache = /* @__PURE__ */ new Map();
1252
+ async function blobUrlToDataUrl(blobUrl) {
1253
+ if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
1254
+ const res = await fetch(blobUrl);
1255
+ if (!res.ok) throw new Error(`[SnapDOM] HTTP ${res.status} on blob fetch (${blobUrl})`);
1256
+ const blob = await res.blob();
1257
+ const dataUrl = await new Promise((resolve, reject) => {
1258
+ const fr = new FileReader();
1259
+ fr.onloadend = () => {
1260
+ const v = fr.result;
1261
+ if (typeof v === "string" && v.startsWith("data:")) resolve(v);
1262
+ else reject(new Error("[SnapDOM] Invalid data URL from blob"));
1263
+ };
1264
+ fr.onerror = () => reject(new Error("[SnapDOM] FileReader error"));
1265
+ fr.readAsDataURL(blob);
1266
+ });
1267
+ _blobToDataUrlCache.set(blobUrl, dataUrl);
1268
+ return dataUrl;
1269
+ }
1270
+ var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
1271
+ async function replaceBlobUrlsInCssText(cssText) {
1272
+ if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
1273
+ const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
1274
+ if (uniques.length === 0) return cssText;
1275
+ let out = cssText;
1276
+ for (const u of uniques) {
1277
+ try {
1278
+ const d = await blobUrlToDataUrl(u);
1279
+ out = out.split(u).join(d);
1280
+ } catch {
1281
+ }
1282
+ }
1283
+ return out;
1284
+ }
1285
+ function isBlobUrl(u) {
1286
+ return typeof u === "string" && u.startsWith("blob:");
1287
+ }
1288
+ function parseSrcset(srcset) {
1289
+ return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
1290
+ const m = item.match(/^(\S+)(\s+.+)?$/);
1291
+ return m ? { url: m[1], desc: m[2] || "" } : null;
1292
+ }).filter(Boolean);
1293
+ }
1294
+ function stringifySrcset(parts) {
1295
+ return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
1296
+ }
1297
+ async function resolveBlobUrlsInTree(root) {
1298
+ if (!root) return;
1299
+ const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
1300
+ for (const img of imgs) {
1301
+ try {
1302
+ const srcAttr = img.getAttribute("src");
1303
+ const effective = srcAttr || img.currentSrc || "";
1304
+ if (isBlobUrl(effective)) {
1305
+ const data = await blobUrlToDataUrl(effective);
1306
+ img.setAttribute("src", data);
1307
+ }
1308
+ const srcset = img.getAttribute("srcset");
1309
+ if (srcset && srcset.includes("blob:")) {
1310
+ const parts = parseSrcset(srcset);
1311
+ let changed = false;
1312
+ for (const p of parts) {
1313
+ if (isBlobUrl(p.url)) {
1314
+ try {
1315
+ p.url = await blobUrlToDataUrl(p.url);
1316
+ changed = true;
1317
+ } catch {
1318
+ }
1319
+ }
1320
+ }
1321
+ if (changed) img.setAttribute("srcset", stringifySrcset(parts));
1322
+ }
1323
+ } catch {
1324
+ }
1325
+ }
1326
+ const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
1327
+ for (const node of svgImages) {
1328
+ try {
1329
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
1330
+ const href = node.getAttribute("href") || node.getAttributeNS?.(XLINK_NS, "href");
1331
+ if (isBlobUrl(href)) {
1332
+ const d = await blobUrlToDataUrl(href);
1333
+ node.setAttribute("href", d);
1334
+ node.removeAttributeNS?.(XLINK_NS, "href");
1335
+ }
1336
+ } catch {
1337
+ }
1338
+ }
1339
+ const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
1340
+ for (const el of styled) {
1341
+ try {
1342
+ const styleText = el.getAttribute("style");
1343
+ if (styleText && styleText.includes("blob:")) {
1344
+ const replaced = await replaceBlobUrlsInCssText(styleText);
1345
+ el.setAttribute("style", replaced);
1346
+ }
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
1351
+ for (const s of styleTags) {
1352
+ try {
1353
+ const css = s.textContent || "";
1354
+ if (css.includes("blob:")) {
1355
+ s.textContent = await replaceBlobUrlsInCssText(css);
1356
+ }
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ const urlAttrs = ["poster"];
1361
+ for (const attr of urlAttrs) {
1362
+ const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
1363
+ for (const n of nodes) {
1364
+ try {
1365
+ const u = n.getAttribute(attr);
1366
+ if (isBlobUrl(u)) {
1367
+ n.setAttribute(attr, await blobUrlToDataUrl(u));
1368
+ }
1369
+ } catch {
1370
+ }
1371
+ }
1372
+ }
1053
1373
  }
1054
1374
 
1055
1375
  // src/modules/images.js
1056
1376
  async function inlineImages(clone, options = {}) {
1057
1377
  const imgs = Array.from(clone.querySelectorAll("img"));
1058
1378
  const processImg = async (img) => {
1379
+ if (!img.getAttribute("src")) {
1380
+ const eff = img.currentSrc || img.src || "";
1381
+ if (eff) img.setAttribute("src", eff);
1382
+ }
1383
+ img.removeAttribute("srcset");
1384
+ img.removeAttribute("sizes");
1059
1385
  const src = img.src;
1060
1386
  try {
1061
1387
  const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
@@ -1076,7 +1402,7 @@ async function inlineImages(clone, options = {}) {
1076
1402
  }
1077
1403
 
1078
1404
  // src/modules/background.js
1079
- async function inlineBackgroundImages(source, clone, options = {}) {
1405
+ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
1080
1406
  const queue = [[source, clone]];
1081
1407
  const imageProps = [
1082
1408
  "background-image",
@@ -1086,13 +1412,27 @@ async function inlineBackgroundImages(source, clone, options = {}) {
1086
1412
  "mask-source",
1087
1413
  "mask-box-image-source",
1088
1414
  "mask-border-source",
1089
- "-webkit-mask-box-image-source"
1415
+ "-webkit-mask-box-image-source",
1416
+ "border-image",
1417
+ "border-image-source",
1418
+ "border-image-slice",
1419
+ "border-image-width",
1420
+ "border-image-outset",
1421
+ "border-image-repeat"
1090
1422
  ];
1091
1423
  while (queue.length) {
1092
1424
  const [srcNode, cloneNode] = queue.shift();
1093
- const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
1094
- if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
1425
+ const style = styleCache.get(srcNode) || getStyle(srcNode);
1426
+ if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
1427
+ const hasBorderImage = (() => {
1428
+ const bi = style.getPropertyValue("border-image");
1429
+ const bis = style.getPropertyValue("border-image-source");
1430
+ return bi && bi !== "none" || bis && bis !== "none";
1431
+ })();
1095
1432
  for (const prop of imageProps) {
1433
+ if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
1434
+ continue;
1435
+ }
1096
1436
  const val = style.getPropertyValue(prop);
1097
1437
  if (!val || val === "none") continue;
1098
1438
  const splits = splitBackgroundImage(val);
@@ -1119,13 +1459,13 @@ async function inlineBackgroundImages(source, clone, options = {}) {
1119
1459
  async function captureDOM(element, options = {}) {
1120
1460
  if (!element) throw new Error("Element cannot be null or undefined");
1121
1461
  cache.reset();
1122
- const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
1123
- let clone, classCSS;
1462
+ const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "", localFonts = [] } = options;
1463
+ let clone, classCSS, styleCache;
1124
1464
  let fontsCSS = "";
1125
1465
  let baseCSS = "";
1126
1466
  let dataURL;
1127
1467
  let svgString;
1128
- ({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));
1468
+ ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
1129
1469
  await new Promise((resolve) => {
1130
1470
  idle(async () => {
1131
1471
  await inlineImages(clone, options);
@@ -1134,14 +1474,14 @@ async function captureDOM(element, options = {}) {
1134
1474
  });
1135
1475
  await new Promise((resolve) => {
1136
1476
  idle(async () => {
1137
- await inlineBackgroundImages(element, clone, options);
1477
+ await inlineBackgroundImages(element, clone, styleCache, options);
1138
1478
  resolve();
1139
1479
  }, { fast });
1140
1480
  });
1141
1481
  if (embedFonts) {
1142
1482
  await new Promise((resolve) => {
1143
1483
  idle(async () => {
1144
- fontsCSS = await embedCustomFonts();
1484
+ fontsCSS = await embedCustomFonts({ localFonts, useProxy });
1145
1485
  resolve();
1146
1486
  }, { fast });
1147
1487
  });
@@ -1218,12 +1558,14 @@ async function captureDOM(element, options = {}) {
1218
1558
  }
1219
1559
 
1220
1560
  // src/api/snapdom.js
1221
- async function toImg(url, { dpr = 1, scale = 1 }) {
1561
+ async function toImg(url, { scale = 1 } = {}) {
1222
1562
  const img = new Image();
1223
1563
  img.src = url;
1224
1564
  await img.decode();
1225
- img.width = img.width * scale;
1226
- img.height = img.height * scale;
1565
+ if (scale !== 1) {
1566
+ img.style.width = `${img.naturalWidth * scale}px`;
1567
+ img.style.height = `${img.naturalHeight * scale}px`;
1568
+ }
1227
1569
  return img;
1228
1570
  }
1229
1571
  async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
@@ -1246,8 +1588,8 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
1246
1588
  if (appended) img.remove();
1247
1589
  throw new Error("Image failed to load or has no dimensions");
1248
1590
  }
1249
- const width = img.width * scale;
1250
- const height = img.height * scale;
1591
+ const width = img.naturalWidth * scale;
1592
+ const height = img.naturalHeight * scale;
1251
1593
  const canvas = document.createElement("canvas");
1252
1594
  canvas.width = Math.ceil(width * dpr);
1253
1595
  canvas.height = Math.ceil(height * dpr);
@@ -1339,19 +1681,19 @@ async function snapdom(element, options = {}) {
1339
1681
  }
1340
1682
  snapdom.capture = async (el, options = {}) => {
1341
1683
  const url = await captureDOM(el, options);
1342
- const dpr = window.devicePixelRatio || 1;
1684
+ const dpr = options.dpr ?? (window.devicePixelRatio || 1);
1343
1685
  const scale = options.scale || 1;
1344
1686
  return {
1345
1687
  url,
1346
1688
  options,
1347
1689
  toRaw: () => url,
1348
- toImg: () => toImg(url, { dpr, scale }),
1349
- toCanvas: () => toCanvas(url, { dpr, scale }),
1350
- toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
1351
- toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
1352
- toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
1353
- toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
1354
- download: ({ format = "png", filename = "capture", backgroundColor } = {}) => download(url, { dpr, scale, backgroundColor, format, filename })
1690
+ toImg: (opts = {}) => toImg(url, { dpr, scale, ...opts }),
1691
+ toCanvas: (opts = {}) => toCanvas(url, { dpr, scale, ...opts }),
1692
+ toBlob: (opts = {}) => toBlob(url, { dpr, scale, ...opts }),
1693
+ toPng: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "png"),
1694
+ toJpg: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "jpeg"),
1695
+ toWebp: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "webp"),
1696
+ download: ({ format = "png", filename = "snapDOM", backgroundColor, ...opts } = {}) => download(url, { dpr, scale, format, filename, backgroundColor, ...opts })
1355
1697
  };
1356
1698
  };
1357
1699
  snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
@@ -1374,12 +1716,21 @@ snapdom.download = async (el, options = {}) => {
1374
1716
 
1375
1717
  // src/api/preCache.js
1376
1718
  async function preCache(root = document, options = {}) {
1377
- const { embedFonts = true, reset = false } = options;
1719
+ const { embedFonts = true, reset = false, useProxy } = options;
1378
1720
  if (reset) {
1379
- cache.reset();
1721
+ cache.image.clear();
1722
+ cache.background.clear();
1723
+ cache.resource.clear();
1724
+ cache.defaultStyle.clear();
1725
+ cache.baseStyle.clear();
1726
+ cache.font.clear();
1727
+ cache.computedStyle = /* @__PURE__ */ new WeakMap();
1380
1728
  return;
1381
1729
  }
1382
- await document.fonts.ready;
1730
+ try {
1731
+ await document.fonts.ready;
1732
+ } catch {
1733
+ }
1383
1734
  precacheCommonTags();
1384
1735
  let imgEls = [], allEls = [];
1385
1736
  if (root?.querySelectorAll) {
@@ -1388,33 +1739,41 @@ async function preCache(root = document, options = {}) {
1388
1739
  }
1389
1740
  const promises = [];
1390
1741
  for (const img of imgEls) {
1391
- const src = img.src;
1742
+ const src = img?.src;
1743
+ if (!src) continue;
1392
1744
  if (!cache.image.has(src)) {
1393
- promises.push(
1394
- fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => cache.image.set(src, dataURL)).catch(() => {
1395
- })
1396
- );
1745
+ const p = Promise.resolve().then(() => fetchImage(src, { useProxy })).then((dataURL) => {
1746
+ cache.image.set(src, dataURL);
1747
+ }).catch(() => {
1748
+ });
1749
+ promises.push(p);
1397
1750
  }
1398
1751
  }
1399
1752
  for (const el of allEls) {
1400
- const bg = getStyle(el).backgroundImage;
1753
+ let bg = "";
1754
+ try {
1755
+ bg = getStyle(el).backgroundImage;
1756
+ } catch {
1757
+ }
1401
1758
  if (bg && bg !== "none") {
1402
1759
  const bgSplits = splitBackgroundImage(bg);
1403
1760
  for (const entry of bgSplits) {
1404
- const isUrl = entry.startsWith("url(");
1405
- if (isUrl) {
1406
- promises.push(
1407
- inlineSingleBackgroundEntry(entry, options).catch(() => {
1408
- })
1409
- );
1761
+ if (entry.startsWith("url(")) {
1762
+ const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
1763
+ });
1764
+ promises.push(p);
1410
1765
  }
1411
1766
  }
1412
1767
  }
1413
1768
  }
1414
1769
  if (embedFonts) {
1415
- await embedCustomFonts({ preCached: true });
1770
+ try {
1771
+ await embedCustomFonts({ preCached: true, localFonts: options.localFonts, useProxy: options.useProxy });
1772
+ } catch {
1773
+ }
1774
+ ;
1416
1775
  }
1417
- await Promise.all(promises);
1776
+ await Promise.allSettled(promises);
1418
1777
  }
1419
1778
  export {
1420
1779
  preCache,