@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.js CHANGED
@@ -1,13 +1,15 @@
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
  **/
8
8
 
9
9
  (() => {
10
10
  var __defProp = Object.defineProperty;
11
+ var __defProps = Object.defineProperties;
12
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
11
13
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
12
14
  var __hasOwnProp = Object.prototype.hasOwnProperty;
13
15
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -23,6 +25,7 @@
23
25
  }
24
26
  return a;
25
27
  };
28
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
26
29
  var __objRest = (source, exclude) => {
27
30
  var target = {};
28
31
  for (var prop in source)
@@ -67,18 +70,10 @@
67
70
  font: /* @__PURE__ */ new Set(),
68
71
  snapshot: /* @__PURE__ */ new WeakMap(),
69
72
  snapshotKey: /* @__PURE__ */ new Map(),
70
- preStyleMap: /* @__PURE__ */ new Map(),
71
- preStyle: /* @__PURE__ */ new WeakMap(),
72
- preNodeMap: /* @__PURE__ */ new Map(),
73
73
  reset: resetCache
74
74
  };
75
75
  function resetCache() {
76
76
  cache.computedStyle = /* @__PURE__ */ new WeakMap();
77
- cache.snapshot = /* @__PURE__ */ new WeakMap();
78
- cache.snapshotKey.clear();
79
- cache.preStyleMap.clear();
80
- cache.preStyle = /* @__PURE__ */ new WeakMap();
81
- cache.preNodeMap.clear();
82
77
  }
83
78
 
84
79
  // src/utils/cssTools.js
@@ -130,7 +125,7 @@
130
125
  if (cache.defaultStyle.has(tagName)) {
131
126
  return cache.defaultStyle.get(tagName);
132
127
  }
133
- const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
128
+ const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
134
129
  if (skipTags.has(tagName)) {
135
130
  const empty = {};
136
131
  cache.defaultStyle.set(tagName, empty);
@@ -160,10 +155,14 @@
160
155
  cache.defaultStyle.set(tagName, defaults);
161
156
  return defaults;
162
157
  }
158
+ var IGNORED_PROPS = /* @__PURE__ */ new Set([
159
+ "-webkit-locale"
160
+ ]);
163
161
  function getStyleKey(snapshot, tagName, compress = false) {
164
162
  const entries = [];
165
163
  const defaultStyles = getDefaultStyleForTag(tagName);
166
164
  for (let [prop, value] of Object.entries(snapshot)) {
165
+ if (IGNORED_PROPS.has(prop)) continue;
167
166
  if (!compress) {
168
167
  if (value) {
169
168
  entries.push(`${prop}:${value}`);
@@ -208,11 +207,12 @@
208
207
  }
209
208
  return css;
210
209
  }
211
- function generateCSSClasses() {
212
- const keySet = new Set(cache.preStyleMap.values());
210
+ function generateCSSClasses(styleMap) {
211
+ const keySet = new Set(styleMap.values());
213
212
  const classMap = /* @__PURE__ */ new Map();
214
213
  let counter = 1;
215
214
  for (const key of keySet) {
215
+ if (!key.trim()) continue;
216
216
  classMap.set(key, `c${counter++}`);
217
217
  }
218
218
  return classMap;
@@ -280,7 +280,29 @@
280
280
  if (url.startsWith("#")) return null;
281
281
  return url;
282
282
  }
283
- function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
283
+ function fetchResource(_0) {
284
+ return __async(this, arguments, function* (url, { useProxy = "" } = {}) {
285
+ function doFetch(u) {
286
+ return __async(this, null, function* () {
287
+ const res = yield fetch(u);
288
+ if (!res.ok) throw new Error(`[snapdom] Failed to fetch resource: ${u}`);
289
+ return res;
290
+ });
291
+ }
292
+ try {
293
+ return yield doFetch(url);
294
+ } catch (e) {
295
+ if (useProxy && typeof useProxy === "string") {
296
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
297
+ return doFetch(proxied);
298
+ }
299
+ throw e;
300
+ }
301
+ });
302
+ }
303
+ var _inflight = /* @__PURE__ */ new Map();
304
+ var _errorCache = /* @__PURE__ */ new Map();
305
+ function fetchImage(src, { timeout = 3e3, useProxy = "", errorTTL = 8e3 } = {}) {
284
306
  function getCrossOriginMode(url) {
285
307
  try {
286
308
  const parsed = new URL(url, window.location.href);
@@ -289,107 +311,157 @@
289
311
  return "anonymous";
290
312
  }
291
313
  }
292
- function fetchWithFallback(url) {
293
- return __async(this, null, function* () {
294
- const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
314
+ const ok = (data) => ({ ok: true, data });
315
+ const fail = (e) => ({ ok: false, error: e instanceof Error ? e : new Error(String(e)) });
316
+ function fetchBlobAsDataURLSafe(fetchUrl) {
317
+ try {
318
+ return fetch(fetchUrl, {
295
319
  mode: "cors",
296
320
  credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
297
- }).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
298
- const reader = new FileReader();
299
- reader.onloadend = () => {
300
- const base64 = reader.result;
301
- if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
302
- reject(new Error("Invalid image data URL"));
303
- return;
304
- }
305
- resolve(base64);
306
- };
307
- reader.onerror = () => reject(new Error("FileReader error"));
308
- reader.readAsDataURL(blob);
309
- }));
310
- try {
311
- return yield fetchBlobAsDataURL(url);
312
- } catch (e) {
313
- if (useProxy && typeof useProxy === "string") {
314
- const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
315
- try {
316
- return yield fetchBlobAsDataURL(proxied);
317
- } catch (e2) {
318
- throw new Error("[SnapDOM - fetchImage] CORS restrictions prevented image capture (even via proxy)");
319
- }
320
- } else {
321
- throw new Error("[SnapDOM - fetchImage] Fetch fallback failed and no proxy provided");
322
- }
321
+ }).then((r) => {
322
+ if (!r.ok) return fail(new Error("HTTP " + r.status));
323
+ return r.blob().then((blob) => new Promise((resolve) => {
324
+ const reader = new FileReader();
325
+ reader.onloadend = () => {
326
+ const base64 = reader.result;
327
+ if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
328
+ resolve(fail(new Error("Invalid image data URL")));
329
+ } else {
330
+ resolve(ok(base64));
331
+ }
332
+ };
333
+ reader.onerror = () => resolve(fail(new Error("FileReader error")));
334
+ reader.readAsDataURL(blob);
335
+ }));
336
+ }).catch((e) => fail(e));
337
+ } catch (e) {
338
+ return Promise.resolve(fail(e));
339
+ }
340
+ }
341
+ function fetchWithFallbackOnceSafe(url) {
342
+ return fetchBlobAsDataURLSafe(url).then((r) => {
343
+ if (r.ok) return r;
344
+ if (useProxy && typeof useProxy === "string") {
345
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
346
+ return fetchBlobAsDataURLSafe(proxied).then((r2) => {
347
+ if (r2.ok) return r2;
348
+ return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
349
+ });
323
350
  }
351
+ return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
324
352
  });
325
353
  }
326
- const crossOriginValue = getCrossOriginMode(src);
327
- if (cache.image.has(src)) {
328
- return Promise.resolve(cache.image.get(src));
354
+ const now = Date.now();
355
+ const until = _errorCache.get(src);
356
+ if (until && until > now) {
357
+ const pr = Promise.reject(new Error("[SnapDOM - fetchImage] Recently failed (cooldown)."));
358
+ pr.catch(() => {
359
+ });
360
+ return pr;
329
361
  }
330
- const isDataURI = src.startsWith("data:image/");
331
- if (isDataURI) {
362
+ if (_inflight.has(src)) return _inflight.get(src);
363
+ const crossOriginValue = getCrossOriginMode(src);
364
+ if (cache.image.has(src)) return Promise.resolve(cache.image.get(src));
365
+ if (src.startsWith("data:image/")) {
332
366
  cache.image.set(src, src);
333
367
  return Promise.resolve(src);
334
368
  }
335
- const isSVG = /\.svg(\?.*)?$/i.test(src);
336
- if (isSVG) {
337
- return (() => __async(null, null, function* () {
338
- try {
339
- const response = yield fetch(src, {
340
- mode: "cors",
341
- credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
342
- });
343
- const svgText = yield response.text();
344
- const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
345
- cache.image.set(src, encoded);
346
- return encoded;
347
- } catch (e) {
348
- return fetchWithFallback(src);
369
+ if (/\.svg(\?.*)?$/i.test(src)) {
370
+ const p2 = (() => __async(null, null, function* () {
371
+ const direct = yield (() => __async(null, null, function* () {
372
+ try {
373
+ const res = yield fetch(src, {
374
+ mode: "cors",
375
+ credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
376
+ });
377
+ if (!res.ok) return fail(new Error("HTTP " + res.status));
378
+ const svgText = yield res.text();
379
+ return ok(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`);
380
+ } catch (e) {
381
+ return fail(e);
382
+ }
383
+ }))();
384
+ if (direct.ok) {
385
+ cache.image.set(src, direct.data);
386
+ return direct.data;
387
+ }
388
+ const via = yield fetchWithFallbackOnceSafe(src);
389
+ if (via.ok) {
390
+ cache.image.set(src, via.data);
391
+ return via.data;
349
392
  }
393
+ _errorCache.set(src, now + errorTTL);
394
+ return Promise.reject(via.error);
350
395
  }))();
396
+ _inflight.set(src, p2);
397
+ p2.finally(() => _inflight.delete(src));
398
+ p2.catch(() => {
399
+ });
400
+ return p2;
351
401
  }
352
- return new Promise((resolve, reject) => {
353
- const timeoutId = setTimeout(() => {
354
- reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
355
- }, timeout);
356
- const image = new Image();
357
- image.crossOrigin = crossOriginValue;
358
- image.onload = () => __async(null, null, function* () {
402
+ const p = new Promise((resolve, reject) => {
403
+ let finished = false;
404
+ const img = new Image();
405
+ const finish = (fn) => (arg) => {
406
+ if (finished) return;
407
+ finished = true;
359
408
  clearTimeout(timeoutId);
360
- try {
361
- yield image.decode();
362
- const canvas = document.createElement("canvas");
363
- canvas.width = image.width;
364
- canvas.height = image.height;
365
- const ctx = canvas.getContext("2d");
366
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
367
- const dataURL = canvas.toDataURL("image/png");
368
- cache.image.set(src, dataURL);
369
- resolve(dataURL);
370
- } catch (e) {
409
+ img.onload = img.onerror = null;
410
+ fn(arg);
411
+ };
412
+ const onSuccess = (d) => {
413
+ cache.image.set(src, d);
414
+ resolve(d);
415
+ };
416
+ const onFinalError = (e) => {
417
+ _errorCache.set(src, Date.now() + errorTTL);
418
+ reject(e);
419
+ };
420
+ const timeoutId = setTimeout(
421
+ finish(() => {
422
+ fetchWithFallbackOnceSafe(src).then((r) => {
423
+ if (r.ok) onSuccess(r.data);
424
+ else onFinalError(new Error("Image load timed out"));
425
+ });
426
+ }),
427
+ timeout
428
+ );
429
+ img.crossOrigin = crossOriginValue;
430
+ img.onload = finish(() => {
431
+ Promise.resolve(img.decode()).then(() => {
371
432
  try {
372
- const fallbackDataURL = yield fetchWithFallback(src);
373
- cache.image.set(src, fallbackDataURL);
374
- resolve(fallbackDataURL);
375
- } catch (e2) {
376
- reject(e2);
433
+ const canvas = document.createElement("canvas");
434
+ canvas.width = img.naturalWidth || img.width;
435
+ canvas.height = img.naturalHeight || img.height;
436
+ const ctx = canvas.getContext("2d");
437
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
438
+ onSuccess(canvas.toDataURL("image/png"));
439
+ } catch (e) {
440
+ fetchWithFallbackOnceSafe(src).then((r) => {
441
+ if (r.ok) onSuccess(r.data);
442
+ else onFinalError(r.error);
443
+ });
377
444
  }
378
- }
445
+ }).catch(() => {
446
+ fetchWithFallbackOnceSafe(src).then((r) => {
447
+ if (r.ok) onSuccess(r.data);
448
+ else onFinalError(r.error);
449
+ });
450
+ });
379
451
  });
380
- image.onerror = () => __async(null, null, function* () {
381
- clearTimeout(timeoutId);
382
- console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
383
- try {
384
- const fallbackDataURL = yield fetchWithFallback(src);
385
- cache.image.set(src, fallbackDataURL);
386
- resolve(fallbackDataURL);
387
- } catch (e) {
388
- reject(e);
389
- }
452
+ img.onerror = finish(() => {
453
+ fetchWithFallbackOnceSafe(src).then((r) => {
454
+ if (r.ok) onSuccess(r.data);
455
+ else onFinalError(r.error);
456
+ });
390
457
  });
391
- image.src = src;
458
+ img.src = src;
392
459
  });
460
+ _inflight.set(src, p);
461
+ p.finally(() => _inflight.delete(src));
462
+ p.catch(() => {
463
+ });
464
+ return p;
393
465
  }
394
466
  function snapshotComputedStyle(style) {
395
467
  const snap = {};
@@ -446,8 +518,11 @@
446
518
  }
447
519
 
448
520
  // src/modules/styles.js
521
+ var snapshotCache = /* @__PURE__ */ new WeakMap();
522
+ var snapshotKeyCache = /* @__PURE__ */ new Map();
449
523
  function snapshotComputedStyleFull(style) {
450
524
  const result = {};
525
+ const computedVisibility = style.getPropertyValue("visibility");
451
526
  for (let i = 0; i < style.length; i++) {
452
527
  const prop = style[i];
453
528
  let val = style.getPropertyValue(prop);
@@ -456,33 +531,48 @@
456
531
  }
457
532
  result[prop] = val;
458
533
  }
534
+ if (computedVisibility === "hidden") {
535
+ result.opacity = "0";
536
+ }
459
537
  return result;
460
538
  }
461
- function inlineAllStyles(source, clone, compress) {
539
+ function inlineAllStyles(source, clone, styleMap, cache2, compress) {
462
540
  var _a;
463
541
  if (source.tagName === "STYLE") return;
464
- if (!cache.preStyle.has(source)) {
465
- cache.preStyle.set(source, getStyle(source));
542
+ if (!cache2.has(source)) {
543
+ cache2.set(source, getStyle(source));
466
544
  }
467
- const style = cache.preStyle.get(source);
468
- if (!cache.snapshot.has(source)) {
545
+ const style = cache2.get(source);
546
+ if (!snapshotCache.has(source)) {
469
547
  const snapshot2 = snapshotComputedStyleFull(style);
470
- cache.snapshot.set(source, snapshot2);
548
+ snapshotCache.set(source, snapshot2);
471
549
  }
472
- const snapshot = cache.snapshot.get(source);
550
+ const snapshot = snapshotCache.get(source);
473
551
  const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
474
- if (cache.snapshotKey.has(hash)) {
475
- cache.preStyleMap.set(clone, cache.snapshotKey.get(hash));
552
+ if (snapshotKeyCache.has(hash)) {
553
+ styleMap.set(clone, snapshotKeyCache.get(hash));
476
554
  return;
477
555
  }
478
556
  const tagName = ((_a = source.tagName) == null ? void 0 : _a.toLowerCase()) || "div";
479
557
  const key = getStyleKey(snapshot, tagName, compress);
480
- cache.snapshotKey.set(hash, key);
481
- cache.preStyleMap.set(clone, key);
558
+ snapshotKeyCache.set(hash, key);
559
+ styleMap.set(clone, key);
482
560
  }
483
561
 
484
562
  // src/core/clone.js
485
- function deepClone(node, compress, options = {}, originalRoot) {
563
+ function freezeImgSrcset(original, cloned) {
564
+ try {
565
+ const chosen = original.currentSrc || original.src || "";
566
+ if (!chosen) return;
567
+ cloned.setAttribute("src", chosen);
568
+ cloned.removeAttribute("srcset");
569
+ cloned.removeAttribute("sizes");
570
+ cloned.loading = "eager";
571
+ cloned.decoding = "sync";
572
+ } catch (e) {
573
+ }
574
+ }
575
+ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
486
576
  var _a, _b;
487
577
  if (!node) throw new Error("Invalid node");
488
578
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
@@ -532,8 +622,8 @@
532
622
  }
533
623
  if (node.getAttribute("data-capture") === "placeholder") {
534
624
  const clone2 = node.cloneNode(false);
535
- cache.preNodeMap.set(clone2, node);
536
- inlineAllStyles(node, clone2, compress);
625
+ nodeMap.set(clone2, node);
626
+ inlineAllStyles(node, clone2, styleMap, styleCache, compress);
537
627
  const placeholder = document.createElement("div");
538
628
  placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
539
629
  placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
@@ -546,14 +636,17 @@
546
636
  img.src = dataURL;
547
637
  img.width = node.width;
548
638
  img.height = node.height;
549
- cache.preNodeMap.set(img, node);
550
- inlineAllStyles(node, img, compress);
639
+ nodeMap.set(img, node);
640
+ inlineAllStyles(node, img, styleMap, styleCache, compress);
551
641
  return img;
552
642
  }
553
643
  let clone;
554
644
  try {
555
645
  clone = node.cloneNode(false);
556
- cache.preNodeMap.set(clone, node);
646
+ nodeMap.set(clone, node);
647
+ if (node.tagName === "IMG") {
648
+ freezeImgSrcset(node, clone);
649
+ }
557
650
  } catch (err) {
558
651
  console.error("[Snapdom] Failed to clone node:", node, err);
559
652
  throw err;
@@ -578,7 +671,7 @@
578
671
  if (node instanceof HTMLSelectElement) {
579
672
  pendingSelectValue = node.value;
580
673
  }
581
- inlineAllStyles(node, clone, compress);
674
+ inlineAllStyles(node, clone, styleMap, styleCache, compress);
582
675
  if (node.shadowRoot) {
583
676
  const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
584
677
  if (hasSlot) {
@@ -586,8 +679,7 @@
586
679
  if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
587
680
  const cssText = child.textContent || "";
588
681
  if (cssText.trim() && compress) {
589
- if (!cache.preStyle) cache.preStyle = /* @__PURE__ */ new WeakMap();
590
- cache.preStyle.set(child, cssText);
682
+ styleCache.set(child, cssText);
591
683
  }
592
684
  }
593
685
  }
@@ -597,12 +689,11 @@
597
689
  if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
598
690
  const cssText = child.textContent || "";
599
691
  if (cssText.trim() && compress) {
600
- if (!cache.preStyle) cache.preStyle = /* @__PURE__ */ new WeakMap();
601
- cache.preStyle.set(child, cssText);
692
+ styleCache.set(child, cssText);
602
693
  }
603
694
  continue;
604
695
  }
605
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
696
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
606
697
  if (clonedChild) shadowFrag.appendChild(clonedChild);
607
698
  }
608
699
  clone.appendChild(shadowFrag);
@@ -613,14 +704,14 @@
613
704
  const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
614
705
  const fragment = document.createDocumentFragment();
615
706
  for (const child of nodesToClone) {
616
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
707
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
617
708
  if (clonedChild) fragment.appendChild(clonedChild);
618
709
  }
619
710
  return fragment;
620
711
  }
621
712
  for (const child of node.childNodes) {
622
713
  if (clonedAssignedNodes.has(child)) continue;
623
- const clonedChild = deepClone(child, compress, options, originalRoot || node);
714
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
624
715
  if (clonedChild) clone.appendChild(clonedChild);
625
716
  }
626
717
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
@@ -678,25 +769,38 @@
678
769
  return __async(this, null, function* () {
679
770
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
680
771
  const dpr = window.devicePixelRatio || 1;
681
- const tempCanvas = document.createElement("canvas");
682
- const tempCtx = tempCanvas.getContext("2d");
683
- tempCtx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
684
- const metrics = tempCtx.measureText(unicodeChar);
685
- const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
686
- const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
687
- const height = ascent + descent;
688
- const width = metrics.width;
772
+ yield document.fonts.ready;
773
+ const span = document.createElement("span");
774
+ span.textContent = unicodeChar;
775
+ span.style.position = "absolute";
776
+ span.style.visibility = "hidden";
777
+ span.style.fontFamily = `"${fontFamily}"`;
778
+ span.style.fontWeight = fontWeight || "normal";
779
+ span.style.fontSize = `${fontSize}px`;
780
+ span.style.lineHeight = "1";
781
+ span.style.whiteSpace = "nowrap";
782
+ span.style.padding = "0";
783
+ span.style.margin = "0";
784
+ document.body.appendChild(span);
785
+ const rect = span.getBoundingClientRect();
786
+ const width = Math.ceil(rect.width);
787
+ const height = Math.ceil(rect.height);
788
+ document.body.removeChild(span);
689
789
  const canvas = document.createElement("canvas");
690
- canvas.width = Math.ceil(width * dpr);
691
- canvas.height = Math.ceil(height * dpr);
790
+ canvas.width = width * dpr;
791
+ canvas.height = height * dpr;
692
792
  const ctx = canvas.getContext("2d");
693
793
  ctx.scale(dpr, dpr);
694
- ctx.font = tempCtx.font;
794
+ ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
695
795
  ctx.textAlign = "left";
696
- ctx.textBaseline = "alphabetic";
796
+ ctx.textBaseline = "top";
697
797
  ctx.fillStyle = color;
698
- ctx.fillText(unicodeChar, 0, ascent);
699
- return canvas.toDataURL();
798
+ ctx.fillText(unicodeChar, 0, 0);
799
+ return {
800
+ dataUrl: canvas.toDataURL(),
801
+ width,
802
+ height
803
+ };
700
804
  });
701
805
  }
702
806
  function isStylesheetLoaded(href) {
@@ -715,7 +819,7 @@
715
819
  });
716
820
  }
717
821
  function embedCustomFonts() {
718
- return __async(this, arguments, function* ({ preCached = false } = {}) {
822
+ return __async(this, arguments, function* ({ preCached = false, localFonts = [], useProxy = "" } = {}) {
719
823
  if (cache.resource.has("fonts-embed-css")) {
720
824
  if (preCached) {
721
825
  const style = document.createElement("style");
@@ -725,6 +829,15 @@
725
829
  }
726
830
  return cache.resource.get("fonts-embed-css");
727
831
  }
832
+ const loadedFonts = /* @__PURE__ */ new Set();
833
+ try {
834
+ for (const f of document.fonts) {
835
+ if (f.status === "loaded") {
836
+ loadedFonts.add(`${f.family}__${f.weight || "normal"}__${f.style || "normal"}`);
837
+ }
838
+ }
839
+ } catch (e) {
840
+ }
728
841
  const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
729
842
  const styleImports = [];
730
843
  for (const styleTag of document.querySelectorAll("style")) {
@@ -743,26 +856,48 @@
743
856
  let finalCSS = "";
744
857
  for (const link of links) {
745
858
  try {
746
- const res = yield fetch(link.href);
859
+ const res = yield fetchResource(link.href, { useProxy });
747
860
  const cssText = yield res.text();
748
861
  if (isIconFont(link.href) || isIconFont(cssText)) continue;
749
- const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
750
- const inlinedCSS = yield Promise.all(
751
- Array.from(cssText.matchAll(urlRegex)).map((match) => __async(null, null, function* () {
862
+ const faceRegex = /@font-face[^{}]*{[^}]*}/g;
863
+ let cssFinal = cssText;
864
+ for (const face of cssText.match(faceRegex) || []) {
865
+ const famMatch = face.match(/font-family:\s*([^;]+);/i);
866
+ if (!famMatch) continue;
867
+ const family = famMatch[1].replace(/['"]/g, "").trim();
868
+ const weightMatch = face.match(/font-weight:\s*([^;]+);/i);
869
+ const styleMatch = face.match(/font-style:\s*([^;]+);/i);
870
+ const weight = weightMatch ? weightMatch[1].trim() : "normal";
871
+ const style = styleMatch ? styleMatch[1].trim() : "normal";
872
+ const key = `${family}__${weight}__${style}`;
873
+ const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
874
+ const hasURL = /url\(/i.test(face);
875
+ const hasLocal = /local\(/i.test(face);
876
+ if (!hasURL && hasLocal) {
877
+ continue;
878
+ }
879
+ if (!loadedFonts.has(key)) {
880
+ cssFinal = cssFinal.replace(face, "");
881
+ continue;
882
+ }
883
+ let inlined = face;
884
+ const matches = Array.from(face.matchAll(urlRegex));
885
+ for (const match of matches) {
752
886
  let rawUrl = extractURL(match[0]);
753
- if (!rawUrl) return null;
887
+ if (!rawUrl) continue;
754
888
  let url = rawUrl;
755
889
  if (!url.startsWith("http") && !url.startsWith("data:")) {
756
890
  url = new URL(url, link.href).href;
757
891
  }
758
- if (isIconFont(url)) return null;
892
+ if (isIconFont(url)) continue;
759
893
  if (cache.resource.has(url)) {
760
894
  cache.font.add(url);
761
- return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
895
+ inlined = inlined.replace(match[0], `url(${cache.resource.get(url)})`);
896
+ continue;
762
897
  }
763
- if (cache.font.has(url)) return null;
898
+ if (cache.font.has(url)) continue;
764
899
  try {
765
- const fontRes = yield fetch(url);
900
+ const fontRes = yield fetchResource(url, { useProxy });
766
901
  const blob = yield fontRes.blob();
767
902
  const b64 = yield new Promise((resolve) => {
768
903
  const reader = new FileReader();
@@ -771,16 +906,12 @@
771
906
  });
772
907
  cache.resource.set(url, b64);
773
908
  cache.font.add(url);
774
- return { original: match[0], inlined: `url(${b64})` };
909
+ inlined = inlined.replace(match[0], `url(${b64})`);
775
910
  } catch (e) {
776
911
  console.warn("[snapdom] Failed to fetch font resource:", url);
777
- return null;
778
912
  }
779
- }))
780
- );
781
- let cssFinal = cssText;
782
- for (const r of inlinedCSS) {
783
- if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
913
+ }
914
+ cssFinal = cssFinal.replace(face, inlined);
784
915
  }
785
916
  finalCSS += cssFinal + "\n";
786
917
  } catch (e) {
@@ -795,14 +926,18 @@
795
926
  const src = rule.style.getPropertyValue("src");
796
927
  const family = rule.style.getPropertyValue("font-family");
797
928
  if (!src || isIconFont(family)) continue;
929
+ const weightVal = rule.style.getPropertyValue("font-weight") || "normal";
930
+ const styleVal = rule.style.getPropertyValue("font-style") || "normal";
931
+ const key = `${family}__${weightVal}__${styleVal}`;
798
932
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
799
933
  const localRegex = /local\((["']?)[^)]+?\1\)/g;
800
934
  const hasURL = !!src.match(urlRegex);
801
935
  const hasLocal = !!src.match(localRegex);
802
936
  if (!hasURL && hasLocal) {
803
- finalCSS += `@font-face{font-family:${family};src:${src};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
937
+ finalCSS += `@font-face{font-family:${family};src:${src};font-style:${styleVal};font-weight:${weightVal};}`;
804
938
  continue;
805
939
  }
940
+ if (!loadedFonts.has(key)) continue;
806
941
  let inlinedSrc = src;
807
942
  const matches = Array.from(src.matchAll(urlRegex));
808
943
  for (const match of matches) {
@@ -820,7 +955,7 @@
820
955
  }
821
956
  if (cache.font.has(url)) continue;
822
957
  try {
823
- const res = yield fetch(url);
958
+ const res = yield fetchResource(url, { useProxy });
824
959
  const blob = yield res.blob();
825
960
  const b64 = yield new Promise((resolve) => {
826
961
  const reader = new FileReader();
@@ -834,7 +969,7 @@
834
969
  console.warn("[snapdom] Failed to fetch font URL:", url);
835
970
  }
836
971
  }
837
- finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
972
+ finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleVal};font-weight:${weightVal};}`;
838
973
  }
839
974
  }
840
975
  }
@@ -852,7 +987,7 @@
852
987
  cache.font.add(font._snapdomSrc);
853
988
  } else if (!cache.font.has(font._snapdomSrc)) {
854
989
  try {
855
- const res = yield fetch(font._snapdomSrc);
990
+ const res = yield fetchResource(font._snapdomSrc, { useProxy });
856
991
  const blob = yield res.blob();
857
992
  b64 = yield new Promise((resolve) => {
858
993
  const reader = new FileReader();
@@ -870,6 +1005,32 @@
870
1005
  finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
871
1006
  }
872
1007
  }
1008
+ for (const font of localFonts) {
1009
+ if (!font || typeof font !== "object") continue;
1010
+ const { family, src, weight = "normal", style = "normal" } = font;
1011
+ if (!family || !src) continue;
1012
+ let b64 = src;
1013
+ if (!b64.startsWith("data:")) {
1014
+ try {
1015
+ const res = yield fetchResource(src, { useProxy });
1016
+ const blob = yield res.blob();
1017
+ b64 = yield new Promise((resolve) => {
1018
+ const reader = new FileReader();
1019
+ reader.onload = () => resolve(reader.result);
1020
+ reader.readAsDataURL(blob);
1021
+ });
1022
+ cache.resource.set(src, b64);
1023
+ cache.font.add(src);
1024
+ } catch (e) {
1025
+ console.warn("[snapdom] Failed to load local font:", src);
1026
+ continue;
1027
+ }
1028
+ } else {
1029
+ cache.resource.set(src, b64);
1030
+ cache.font.add(src);
1031
+ }
1032
+ finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};}`;
1033
+ }
873
1034
  if (finalCSS) {
874
1035
  cache.resource.set("fonts-embed-css", finalCSS);
875
1036
  if (preCached) {
@@ -884,19 +1045,25 @@
884
1045
  }
885
1046
 
886
1047
  // src/modules/pseudo.js
887
- function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
1048
+ function inlinePseudoElements(source, clone, styleMap, styleCache, options) {
888
1049
  return __async(this, null, function* () {
1050
+ var _a;
889
1051
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
890
1052
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
891
1053
  try {
892
1054
  const style = getStyle(source, pseudo);
893
1055
  if (!style || typeof style[Symbol.iterator] !== "function") continue;
1056
+ 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";
1057
+ if (isEmptyPseudo) continue;
894
1058
  if (pseudo === "::first-letter") {
895
1059
  const normal = getComputedStyle(source);
896
1060
  const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
897
1061
  if (!isMeaningful) continue;
898
1062
  const textNode = Array.from(clone.childNodes).find(
899
- (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
1063
+ (n) => {
1064
+ var _a2;
1065
+ return n.nodeType === Node.TEXT_NODE && ((_a2 = n.textContent) == null ? void 0 : _a2.trim().length) > 0;
1066
+ }
900
1067
  );
901
1068
  if (!textNode) continue;
902
1069
  const text = textNode.textContent;
@@ -908,57 +1075,55 @@
908
1075
  span.textContent = first;
909
1076
  span.dataset.snapdomPseudo = "::first-letter";
910
1077
  const snapshot2 = snapshotComputedStyle(style);
911
- const key2 = getStyleKey(snapshot2, "span", compress);
912
- cache.preStyleMap.set(span, key2);
1078
+ const key2 = getStyleKey(snapshot2, "span", options);
1079
+ styleMap.set(span, key2);
913
1080
  const restNode = document.createTextNode(rest);
914
1081
  clone.replaceChild(restNode, textNode);
915
1082
  clone.insertBefore(span, restNode);
916
1083
  continue;
917
1084
  }
918
- const content = style.getPropertyValue("content");
919
- const bg = style.getPropertyValue("background-image");
920
- const bgColor = style.getPropertyValue("background-color");
921
- const fontFamily = style.getPropertyValue("font-family");
922
- const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
923
- const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
924
- const color = style.getPropertyValue("color") || "#000";
925
- const display = style.getPropertyValue("display");
926
- const width = parseFloat(style.getPropertyValue("width"));
927
- const height = parseFloat(style.getPropertyValue("height"));
928
- const borderStyle = style.getPropertyValue("border-style");
929
- const transform = style.getPropertyValue("transform");
1085
+ const content = style.content;
1086
+ const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
1087
+ const bg = style.backgroundImage;
1088
+ const bgColor = style.backgroundColor;
1089
+ const fontFamily = style.fontFamily;
1090
+ const fontSize = parseInt(style.fontSize) || 32;
1091
+ const fontWeight = parseInt(style.fontWeight) || false;
1092
+ const color = style.color || "#000";
1093
+ const display = style.display;
1094
+ const width = parseFloat(style.width);
1095
+ const height = parseFloat(style.height);
1096
+ const borderStyle = style.borderStyle;
1097
+ const borderWidth = parseFloat(style.borderWidth);
1098
+ const transform = style.transform;
930
1099
  const isIconFont2 = isIconFont(fontFamily);
931
- let cleanContent;
932
- if (/counter\s*\(|counters\s*\(/.test(content)) {
933
- cleanContent = "- ";
934
- } else {
935
- cleanContent = parseContent(content);
936
- }
937
- const hasContent = content !== "none";
938
- const hasExplicitContent = hasContent && cleanContent !== "";
1100
+ const hasExplicitContent = content !== "none" && cleanContent !== "";
939
1101
  const hasBg = bg && bg !== "none";
940
1102
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
941
1103
  const hasBox = display !== "inline" && (width > 0 || height > 0);
942
- const hasBorder = borderStyle && borderStyle !== "none";
1104
+ const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
943
1105
  const hasTransform = transform && transform !== "none";
944
1106
  const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
945
1107
  if (!shouldRender) continue;
946
1108
  const pseudoEl = document.createElement("span");
947
1109
  pseudoEl.dataset.snapdomPseudo = pseudo;
1110
+ pseudoEl.style.verticalAlign = "middle";
948
1111
  const snapshot = snapshotComputedStyle(style);
949
- const key = getStyleKey(snapshot, "span", compress);
950
- cache.preStyleMap.set(pseudoEl, key);
1112
+ const key = getStyleKey(snapshot, "span", options);
1113
+ styleMap.set(pseudoEl, key);
951
1114
  if (isIconFont2 && cleanContent.length === 1) {
1115
+ const { dataUrl, width: width2, height: height2 } = yield iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
952
1116
  const imgEl = document.createElement("img");
953
- imgEl.src = yield iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
954
- imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
1117
+ imgEl.src = dataUrl;
1118
+ imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
955
1119
  pseudoEl.appendChild(imgEl);
1120
+ clone.dataset.snapdomHasIcon = "true";
956
1121
  } else if (cleanContent.startsWith("url(")) {
957
1122
  const rawUrl = extractURL(cleanContent);
958
- if (rawUrl && rawUrl.trim() !== "") {
1123
+ if (rawUrl == null ? void 0 : rawUrl.trim()) {
959
1124
  try {
960
1125
  const imgEl = document.createElement("img");
961
- const dataUrl = yield fetchImage(safeEncodeURI(rawUrl, { useProxy }));
1126
+ const dataUrl = yield fetchImage(safeEncodeURI(rawUrl), options);
962
1127
  imgEl.src = dataUrl;
963
1128
  imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
964
1129
  pseudoEl.appendChild(imgEl);
@@ -972,19 +1137,21 @@
972
1137
  if (hasBg) {
973
1138
  try {
974
1139
  const bgSplits = splitBackgroundImage(bg);
975
- const newBgParts = yield Promise.all(
976
- bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
977
- );
1140
+ const newBgParts = yield Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
978
1141
  pseudoEl.style.backgroundImage = newBgParts.join(", ");
979
1142
  } catch (e) {
980
1143
  console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
981
1144
  }
982
1145
  }
983
1146
  if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
984
- const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
1147
+ const hasContent2 = pseudoEl.childNodes.length > 0 || ((_a = pseudoEl.textContent) == null ? void 0 : _a.trim()) !== "";
985
1148
  const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
986
1149
  if (!hasVisibleBox) continue;
987
- pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
1150
+ if (pseudo === "::before") {
1151
+ clone.insertBefore(pseudoEl, clone.firstChild);
1152
+ } else {
1153
+ clone.appendChild(pseudoEl);
1154
+ }
988
1155
  } catch (e) {
989
1156
  console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
990
1157
  }
@@ -992,87 +1159,114 @@
992
1159
  const sChildren = Array.from(source.children);
993
1160
  const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
994
1161
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
995
- yield inlinePseudoElements(
996
- sChildren[i],
997
- cChildren[i],
998
- compress,
999
- embedFonts,
1000
- useProxy
1001
- );
1162
+ yield inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, options);
1002
1163
  }
1003
1164
  });
1004
1165
  }
1005
1166
 
1006
1167
  // src/modules/svgDefs.js
1007
- function inlineExternalDef(root) {
1008
- if (!root) return;
1009
- const defsSources = document.querySelectorAll("svg > defs");
1010
- if (!defsSources.length) return;
1011
- root.querySelectorAll("svg").forEach((svg) => {
1012
- const uses = svg.querySelectorAll("use");
1013
- if (!uses.length) return;
1014
- const usedIds = /* @__PURE__ */ new Set();
1015
- uses.forEach((use) => {
1016
- const href = use.getAttribute("xlink:href") || use.getAttribute("href");
1017
- if (href && href.startsWith("#")) usedIds.add(href.slice(1));
1018
- });
1019
- if (!usedIds.size) return;
1020
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1021
- for (const id of usedIds) {
1022
- for (const source of defsSources) {
1023
- const def = source.querySelector(`#${CSS.escape(id)}`);
1024
- if (def) {
1025
- defs.appendChild(def.cloneNode(true));
1026
- break;
1168
+ function inlineExternalDefsAndSymbols(rootElement) {
1169
+ if (!rootElement) return;
1170
+ const usedIds = /* @__PURE__ */ new Set();
1171
+ rootElement.querySelectorAll("use").forEach((use) => {
1172
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
1173
+ if (href && href.startsWith("#")) {
1174
+ usedIds.add(href.slice(1));
1175
+ }
1176
+ });
1177
+ if (!usedIds.size) return;
1178
+ const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
1179
+ const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
1180
+ const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
1181
+ let container = rootElement.querySelector("svg.inline-defs-container");
1182
+ if (!container) {
1183
+ container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1184
+ container.setAttribute("aria-hidden", "true");
1185
+ container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
1186
+ container.classList.add("inline-defs-container");
1187
+ rootElement.insertBefore(container, rootElement.firstChild);
1188
+ }
1189
+ const existingIds = /* @__PURE__ */ new Set();
1190
+ rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
1191
+ existingIds.add(el.id);
1192
+ });
1193
+ usedIds.forEach((id) => {
1194
+ if (existingIds.has(id)) return;
1195
+ const symbol = globalSymbols.find((sym) => sym.id === id);
1196
+ if (symbol) {
1197
+ container.appendChild(symbol.cloneNode(true));
1198
+ existingIds.add(id);
1199
+ return;
1200
+ }
1201
+ for (const defs of globalDefs) {
1202
+ const defEl = defs.querySelector(`#${CSS.escape(id)}`);
1203
+ if (defEl) {
1204
+ let defsContainer = container.querySelector("defs");
1205
+ if (!defsContainer) {
1206
+ defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1207
+ container.appendChild(defsContainer);
1027
1208
  }
1209
+ defsContainer.appendChild(defEl.cloneNode(true));
1210
+ existingIds.add(id);
1211
+ break;
1028
1212
  }
1029
1213
  }
1030
- if (defs.childNodes.length) {
1031
- svg.insertBefore(defs, svg.firstChild);
1032
- }
1033
1214
  });
1034
1215
  }
1035
1216
 
1036
1217
  // src/core/prepare.js
1037
1218
  function prepareClone(_0) {
1038
1219
  return __async(this, arguments, function* (element, compress = false, embedFonts = false, options = {}) {
1039
- var _a;
1220
+ var _a, _b;
1221
+ const styleMap = /* @__PURE__ */ new Map();
1222
+ const styleCache = /* @__PURE__ */ new WeakMap();
1223
+ const nodeMap = /* @__PURE__ */ new Map();
1040
1224
  let clone;
1041
1225
  let classCSS = "";
1226
+ stabilizeLayout(element);
1042
1227
  try {
1043
- clone = deepClone(element, compress, options, element);
1228
+ inlineExternalDefsAndSymbols(element);
1044
1229
  } catch (e) {
1045
- console.warn("deepClone failed:", e);
1046
- throw e;
1230
+ console.warn("inlineExternal defs or symbol failed:", e);
1047
1231
  }
1048
1232
  try {
1049
- yield inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
1233
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
1050
1234
  } catch (e) {
1051
- console.warn("inlinePseudoElements failed:", e);
1235
+ console.warn("deepClone failed:", e);
1236
+ throw e;
1052
1237
  }
1053
1238
  try {
1054
- inlineExternalDef(clone);
1239
+ yield inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
1055
1240
  } catch (e) {
1056
- console.warn("inlineExternalDef failed:", e);
1241
+ console.warn("inlinePseudoElements failed:", e);
1057
1242
  }
1243
+ yield resolveBlobUrlsInTree(clone);
1058
1244
  if (compress) {
1059
- const keyToClass = generateCSSClasses();
1245
+ const keyToClass = generateCSSClasses(styleMap);
1060
1246
  classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
1061
- for (const [node, key] of cache.preStyleMap.entries()) {
1247
+ for (const [node, key] of styleMap.entries()) {
1062
1248
  if (node.tagName === "STYLE") continue;
1249
+ if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
1250
+ node.setAttribute("style", key.replace(/;/g, "; "));
1251
+ continue;
1252
+ }
1063
1253
  const className = keyToClass.get(key);
1064
1254
  if (className) node.classList.add(className);
1065
1255
  const bgImage = (_a = node.style) == null ? void 0 : _a.backgroundImage;
1066
- node.removeAttribute("style");
1256
+ const hasIcon = (_b = node.dataset) == null ? void 0 : _b.snapdomHasIcon;
1067
1257
  if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
1258
+ if (hasIcon) {
1259
+ node.style.verticalAlign = "middle";
1260
+ node.style.display = "inline";
1261
+ }
1068
1262
  }
1069
1263
  } else {
1070
- for (const [node, key] of cache.preStyleMap.entries()) {
1264
+ for (const [node, key] of styleMap.entries()) {
1071
1265
  if (node.tagName === "STYLE") continue;
1072
1266
  node.setAttribute("style", key.replace(/;/g, "; "));
1073
1267
  }
1074
1268
  }
1075
- for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
1269
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
1076
1270
  const scrollX = originalNode.scrollLeft;
1077
1271
  const scrollY = originalNode.scrollTop;
1078
1272
  const hasScroll = scrollX || scrollY;
@@ -1091,9 +1285,9 @@
1091
1285
  cloneNode.appendChild(inner);
1092
1286
  }
1093
1287
  }
1094
- if (element === cache.preNodeMap.get(clone)) {
1095
- const computed = cache.preStyle.get(element) || window.getComputedStyle(element);
1096
- cache.preStyle.set(element, computed);
1288
+ if (element === nodeMap.get(clone)) {
1289
+ const computed = styleCache.get(element) || window.getComputedStyle(element);
1290
+ styleCache.set(element, computed);
1097
1291
  const transform = stripTranslate(computed.transform);
1098
1292
  clone.style.margin = "0";
1099
1293
  clone.style.position = "static";
@@ -1106,13 +1300,155 @@
1106
1300
  clone.style.clear = "none";
1107
1301
  clone.style.transform = transform || "";
1108
1302
  }
1109
- for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
1303
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
1110
1304
  if (originalNode.tagName === "PRE") {
1111
1305
  cloneNode.style.marginTop = "0";
1112
1306
  cloneNode.style.marginBlockStart = "0";
1113
1307
  }
1114
1308
  }
1115
- return { clone, classCSS };
1309
+ return { clone, classCSS, styleCache };
1310
+ });
1311
+ }
1312
+ function stabilizeLayout(element) {
1313
+ const style = getComputedStyle(element);
1314
+ const outlineStyle = style.outlineStyle;
1315
+ const outlineWidth = style.outlineWidth;
1316
+ const borderStyle = style.borderStyle;
1317
+ const borderWidth = style.borderWidth;
1318
+ const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
1319
+ const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
1320
+ if (outlineVisible && borderAbsent) {
1321
+ element.style.border = `${outlineWidth} solid transparent`;
1322
+ }
1323
+ }
1324
+ var _blobToDataUrlCache = /* @__PURE__ */ new Map();
1325
+ function blobUrlToDataUrl(blobUrl) {
1326
+ return __async(this, null, function* () {
1327
+ if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
1328
+ const res = yield fetch(blobUrl);
1329
+ if (!res.ok) throw new Error(`[SnapDOM] HTTP ${res.status} on blob fetch (${blobUrl})`);
1330
+ const blob = yield res.blob();
1331
+ const dataUrl = yield new Promise((resolve, reject) => {
1332
+ const fr = new FileReader();
1333
+ fr.onloadend = () => {
1334
+ const v = fr.result;
1335
+ if (typeof v === "string" && v.startsWith("data:")) resolve(v);
1336
+ else reject(new Error("[SnapDOM] Invalid data URL from blob"));
1337
+ };
1338
+ fr.onerror = () => reject(new Error("[SnapDOM] FileReader error"));
1339
+ fr.readAsDataURL(blob);
1340
+ });
1341
+ _blobToDataUrlCache.set(blobUrl, dataUrl);
1342
+ return dataUrl;
1343
+ });
1344
+ }
1345
+ var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
1346
+ function replaceBlobUrlsInCssText(cssText) {
1347
+ return __async(this, null, function* () {
1348
+ if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
1349
+ const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
1350
+ if (uniques.length === 0) return cssText;
1351
+ let out = cssText;
1352
+ for (const u of uniques) {
1353
+ try {
1354
+ const d = yield blobUrlToDataUrl(u);
1355
+ out = out.split(u).join(d);
1356
+ } catch (e) {
1357
+ }
1358
+ }
1359
+ return out;
1360
+ });
1361
+ }
1362
+ function isBlobUrl(u) {
1363
+ return typeof u === "string" && u.startsWith("blob:");
1364
+ }
1365
+ function parseSrcset(srcset) {
1366
+ return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
1367
+ const m = item.match(/^(\S+)(\s+.+)?$/);
1368
+ return m ? { url: m[1], desc: m[2] || "" } : null;
1369
+ }).filter(Boolean);
1370
+ }
1371
+ function stringifySrcset(parts) {
1372
+ return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
1373
+ }
1374
+ function resolveBlobUrlsInTree(root) {
1375
+ return __async(this, null, function* () {
1376
+ var _a, _b;
1377
+ if (!root) return;
1378
+ const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
1379
+ for (const img of imgs) {
1380
+ try {
1381
+ const srcAttr = img.getAttribute("src");
1382
+ const effective = srcAttr || img.currentSrc || "";
1383
+ if (isBlobUrl(effective)) {
1384
+ const data = yield blobUrlToDataUrl(effective);
1385
+ img.setAttribute("src", data);
1386
+ }
1387
+ const srcset = img.getAttribute("srcset");
1388
+ if (srcset && srcset.includes("blob:")) {
1389
+ const parts = parseSrcset(srcset);
1390
+ let changed = false;
1391
+ for (const p of parts) {
1392
+ if (isBlobUrl(p.url)) {
1393
+ try {
1394
+ p.url = yield blobUrlToDataUrl(p.url);
1395
+ changed = true;
1396
+ } catch (e) {
1397
+ }
1398
+ }
1399
+ }
1400
+ if (changed) img.setAttribute("srcset", stringifySrcset(parts));
1401
+ }
1402
+ } catch (e) {
1403
+ }
1404
+ }
1405
+ const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
1406
+ for (const node of svgImages) {
1407
+ try {
1408
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
1409
+ const href = node.getAttribute("href") || ((_a = node.getAttributeNS) == null ? void 0 : _a.call(node, XLINK_NS, "href"));
1410
+ if (isBlobUrl(href)) {
1411
+ const d = yield blobUrlToDataUrl(href);
1412
+ node.setAttribute("href", d);
1413
+ (_b = node.removeAttributeNS) == null ? void 0 : _b.call(node, XLINK_NS, "href");
1414
+ }
1415
+ } catch (e) {
1416
+ }
1417
+ }
1418
+ const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
1419
+ for (const el of styled) {
1420
+ try {
1421
+ const styleText = el.getAttribute("style");
1422
+ if (styleText && styleText.includes("blob:")) {
1423
+ const replaced = yield replaceBlobUrlsInCssText(styleText);
1424
+ el.setAttribute("style", replaced);
1425
+ }
1426
+ } catch (e) {
1427
+ }
1428
+ }
1429
+ const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
1430
+ for (const s of styleTags) {
1431
+ try {
1432
+ const css = s.textContent || "";
1433
+ if (css.includes("blob:")) {
1434
+ s.textContent = yield replaceBlobUrlsInCssText(css);
1435
+ }
1436
+ } catch (e) {
1437
+ }
1438
+ }
1439
+ const urlAttrs = ["poster"];
1440
+ for (const attr of urlAttrs) {
1441
+ const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
1442
+ for (const n of nodes) {
1443
+ try {
1444
+ const u = n.getAttribute(attr);
1445
+ if (isBlobUrl(u)) {
1446
+ n.setAttribute(attr, yield blobUrlToDataUrl(u));
1447
+ }
1448
+ } catch (e) {
1449
+ }
1450
+ }
1451
+ }
1116
1452
  });
1117
1453
  }
1118
1454
 
@@ -1121,6 +1457,12 @@
1121
1457
  return __async(this, arguments, function* (clone, options = {}) {
1122
1458
  const imgs = Array.from(clone.querySelectorAll("img"));
1123
1459
  const processImg = (img) => __async(null, null, function* () {
1460
+ if (!img.getAttribute("src")) {
1461
+ const eff = img.currentSrc || img.src || "";
1462
+ if (eff) img.setAttribute("src", eff);
1463
+ }
1464
+ img.removeAttribute("srcset");
1465
+ img.removeAttribute("sizes");
1124
1466
  const src = img.src;
1125
1467
  try {
1126
1468
  const dataUrl = yield fetchImage(src, { useProxy: options.useProxy });
@@ -1142,8 +1484,8 @@
1142
1484
  }
1143
1485
 
1144
1486
  // src/modules/background.js
1145
- function inlineBackgroundImages(_0, _1) {
1146
- return __async(this, arguments, function* (source, clone, options = {}) {
1487
+ function inlineBackgroundImages(_0, _1, _2) {
1488
+ return __async(this, arguments, function* (source, clone, styleCache, options = {}) {
1147
1489
  const queue = [[source, clone]];
1148
1490
  const imageProps = [
1149
1491
  "background-image",
@@ -1153,13 +1495,27 @@
1153
1495
  "mask-source",
1154
1496
  "mask-box-image-source",
1155
1497
  "mask-border-source",
1156
- "-webkit-mask-box-image-source"
1498
+ "-webkit-mask-box-image-source",
1499
+ "border-image",
1500
+ "border-image-source",
1501
+ "border-image-slice",
1502
+ "border-image-width",
1503
+ "border-image-outset",
1504
+ "border-image-repeat"
1157
1505
  ];
1158
1506
  while (queue.length) {
1159
1507
  const [srcNode, cloneNode] = queue.shift();
1160
- const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
1161
- if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
1508
+ const style = styleCache.get(srcNode) || getStyle(srcNode);
1509
+ if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
1510
+ const hasBorderImage = (() => {
1511
+ const bi = style.getPropertyValue("border-image");
1512
+ const bis = style.getPropertyValue("border-image-source");
1513
+ return bi && bi !== "none" || bis && bis !== "none";
1514
+ })();
1162
1515
  for (const prop of imageProps) {
1516
+ if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
1517
+ continue;
1518
+ }
1163
1519
  const val = style.getPropertyValue(prop);
1164
1520
  if (!val || val === "none") continue;
1165
1521
  const splits = splitBackgroundImage(val);
@@ -1188,13 +1544,13 @@
1188
1544
  return __async(this, arguments, function* (element, options = {}) {
1189
1545
  if (!element) throw new Error("Element cannot be null or undefined");
1190
1546
  cache.reset();
1191
- const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
1192
- let clone, classCSS;
1547
+ const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "", localFonts = [] } = options;
1548
+ let clone, classCSS, styleCache;
1193
1549
  let fontsCSS = "";
1194
1550
  let baseCSS = "";
1195
1551
  let dataURL;
1196
1552
  let svgString;
1197
- ({ clone, classCSS } = yield prepareClone(element, compress, embedFonts, options));
1553
+ ({ clone, classCSS, styleCache } = yield prepareClone(element, compress, embedFonts, options));
1198
1554
  yield new Promise((resolve) => {
1199
1555
  idle(() => __async(null, null, function* () {
1200
1556
  yield inlineImages(clone, options);
@@ -1203,14 +1559,14 @@
1203
1559
  });
1204
1560
  yield new Promise((resolve) => {
1205
1561
  idle(() => __async(null, null, function* () {
1206
- yield inlineBackgroundImages(element, clone, options);
1562
+ yield inlineBackgroundImages(element, clone, styleCache, options);
1207
1563
  resolve();
1208
1564
  }), { fast });
1209
1565
  });
1210
1566
  if (embedFonts) {
1211
1567
  yield new Promise((resolve) => {
1212
1568
  idle(() => __async(null, null, function* () {
1213
- fontsCSS = yield embedCustomFonts();
1569
+ fontsCSS = yield embedCustomFonts({ localFonts, useProxy });
1214
1570
  resolve();
1215
1571
  }), { fast });
1216
1572
  });
@@ -1288,13 +1644,15 @@
1288
1644
  }
1289
1645
 
1290
1646
  // src/api/snapdom.js
1291
- function toImg(_0, _1) {
1292
- return __async(this, arguments, function* (url, { dpr = 1, scale = 1 }) {
1647
+ function toImg(_0) {
1648
+ return __async(this, arguments, function* (url, { scale = 1 } = {}) {
1293
1649
  const img = new Image();
1294
1650
  img.src = url;
1295
1651
  yield img.decode();
1296
- img.width = img.width * scale;
1297
- img.height = img.height * scale;
1652
+ if (scale !== 1) {
1653
+ img.style.width = `${img.naturalWidth * scale}px`;
1654
+ img.style.height = `${img.naturalHeight * scale}px`;
1655
+ }
1298
1656
  return img;
1299
1657
  });
1300
1658
  }
@@ -1319,8 +1677,8 @@
1319
1677
  if (appended) img.remove();
1320
1678
  throw new Error("Image failed to load or has no dimensions");
1321
1679
  }
1322
- const width = img.width * scale;
1323
- const height = img.height * scale;
1680
+ const width = img.naturalWidth * scale;
1681
+ const height = img.naturalHeight * scale;
1324
1682
  const canvas = document.createElement("canvas");
1325
1683
  canvas.width = Math.ceil(width * dpr);
1326
1684
  canvas.height = Math.ceil(height * dpr);
@@ -1422,20 +1780,24 @@
1422
1780
  });
1423
1781
  }
1424
1782
  snapdom.capture = (_0, ..._1) => __async(null, [_0, ..._1], function* (el, options = {}) {
1783
+ var _a;
1425
1784
  const url = yield captureDOM(el, options);
1426
- const dpr = window.devicePixelRatio || 1;
1785
+ const dpr = (_a = options.dpr) != null ? _a : window.devicePixelRatio || 1;
1427
1786
  const scale = options.scale || 1;
1428
1787
  return {
1429
1788
  url,
1430
1789
  options,
1431
1790
  toRaw: () => url,
1432
- toImg: () => toImg(url, { dpr, scale }),
1433
- toCanvas: () => toCanvas(url, { dpr, scale }),
1434
- toBlob: (options2) => toBlob(url, __spreadValues({ dpr, scale }, options2)),
1435
- toPng: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "png"),
1436
- toJpg: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "jpeg"),
1437
- toWebp: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "webp"),
1438
- download: ({ format = "png", filename = "capture", backgroundColor } = {}) => download(url, { dpr, scale, backgroundColor, format, filename })
1791
+ toImg: (opts = {}) => toImg(url, __spreadValues({ dpr, scale }, opts)),
1792
+ toCanvas: (opts = {}) => toCanvas(url, __spreadValues({ dpr, scale }, opts)),
1793
+ toBlob: (opts = {}) => toBlob(url, __spreadValues({ dpr, scale }, opts)),
1794
+ toPng: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "png"),
1795
+ toJpg: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "jpeg"),
1796
+ toWebp: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "webp"),
1797
+ download: (_b = {}) => {
1798
+ var _c = _b, { format = "png", filename = "snapDOM", backgroundColor } = _c, opts = __objRest(_c, ["format", "filename", "backgroundColor"]);
1799
+ return download(url, __spreadValues({ dpr, scale, format, filename, backgroundColor }, opts));
1800
+ }
1439
1801
  };
1440
1802
  });
1441
1803
  snapdom.toRaw = (el, options) => __async(null, null, function* () {
@@ -1476,12 +1838,21 @@
1476
1838
  // src/api/preCache.js
1477
1839
  function preCache() {
1478
1840
  return __async(this, arguments, function* (root = document, options = {}) {
1479
- const { embedFonts = true, reset = false } = options;
1841
+ const { embedFonts = true, reset = false, useProxy } = options;
1480
1842
  if (reset) {
1481
- cache.reset();
1843
+ cache.image.clear();
1844
+ cache.background.clear();
1845
+ cache.resource.clear();
1846
+ cache.defaultStyle.clear();
1847
+ cache.baseStyle.clear();
1848
+ cache.font.clear();
1849
+ cache.computedStyle = /* @__PURE__ */ new WeakMap();
1482
1850
  return;
1483
1851
  }
1484
- yield document.fonts.ready;
1852
+ try {
1853
+ yield document.fonts.ready;
1854
+ } catch (e) {
1855
+ }
1485
1856
  precacheCommonTags();
1486
1857
  let imgEls = [], allEls = [];
1487
1858
  if (root == null ? void 0 : root.querySelectorAll) {
@@ -1490,33 +1861,41 @@
1490
1861
  }
1491
1862
  const promises = [];
1492
1863
  for (const img of imgEls) {
1493
- const src = img.src;
1864
+ const src = img == null ? void 0 : img.src;
1865
+ if (!src) continue;
1494
1866
  if (!cache.image.has(src)) {
1495
- promises.push(
1496
- fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => cache.image.set(src, dataURL)).catch(() => {
1497
- })
1498
- );
1867
+ const p = Promise.resolve().then(() => fetchImage(src, { useProxy })).then((dataURL) => {
1868
+ cache.image.set(src, dataURL);
1869
+ }).catch(() => {
1870
+ });
1871
+ promises.push(p);
1499
1872
  }
1500
1873
  }
1501
1874
  for (const el of allEls) {
1502
- const bg = getStyle(el).backgroundImage;
1875
+ let bg = "";
1876
+ try {
1877
+ bg = getStyle(el).backgroundImage;
1878
+ } catch (e) {
1879
+ }
1503
1880
  if (bg && bg !== "none") {
1504
1881
  const bgSplits = splitBackgroundImage(bg);
1505
1882
  for (const entry of bgSplits) {
1506
- const isUrl = entry.startsWith("url(");
1507
- if (isUrl) {
1508
- promises.push(
1509
- inlineSingleBackgroundEntry(entry, options).catch(() => {
1510
- })
1511
- );
1883
+ if (entry.startsWith("url(")) {
1884
+ const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, __spreadProps(__spreadValues({}, options), { useProxy }))).catch(() => {
1885
+ });
1886
+ promises.push(p);
1512
1887
  }
1513
1888
  }
1514
1889
  }
1515
1890
  }
1516
1891
  if (embedFonts) {
1517
- yield embedCustomFonts({ preCached: true });
1892
+ try {
1893
+ yield embedCustomFonts({ preCached: true, localFonts: options.localFonts, useProxy: options.useProxy });
1894
+ } catch (e) {
1895
+ }
1896
+ ;
1518
1897
  }
1519
- yield Promise.all(promises);
1898
+ yield Promise.allSettled(promises);
1520
1899
  });
1521
1900
  }
1522
1901