@zumer/snapdom 1.9.10-dev.1 → 1.9.10

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.10-dev.1
4
+ * v.1.9.10
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -42,10 +42,13 @@ function applyCachePolicy(policy = "soft") {
42
42
  cache.session.styleMap = /* @__PURE__ */ new Map();
43
43
  cache.session.nodeMap = /* @__PURE__ */ new Map();
44
44
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
45
- cache.image.clear();
46
- cache.background.clear();
47
- cache.resource.clear();
48
- cache.font.clear();
45
+ cache.computedStyle = /* @__PURE__ */ new WeakMap();
46
+ cache.baseStyle = /* @__PURE__ */ new Map();
47
+ cache.defaultStyle = /* @__PURE__ */ new Map();
48
+ cache.image = /* @__PURE__ */ new Map();
49
+ cache.background = /* @__PURE__ */ new Map();
50
+ cache.resource = /* @__PURE__ */ new Map();
51
+ cache.font = /* @__PURE__ */ new Set();
49
52
  return;
50
53
  }
51
54
  default: {
@@ -637,6 +640,11 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
637
640
  const ctx = _resolveCtx(sessionOrCtx, opts);
638
641
  const resetMode = ctx.options && ctx.options.cache || "auto";
639
642
  if (resetMode !== "disabled") setupInvalidationOnce(document.documentElement);
643
+ if (resetMode === "disabled" && !ctx.session.__bumpedForDisabled) {
644
+ bumpEpoch();
645
+ snapshotKeyCache.clear();
646
+ ctx.session.__bumpedForDisabled = true;
647
+ }
640
648
  if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
641
649
  const author = source.getAttribute?.("style");
642
650
  if (author) clone.setAttribute("style", author);
@@ -658,6 +666,31 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
658
666
  }
659
667
 
660
668
  // src/core/clone.js
669
+ function idleCallback(childList, callback, fast) {
670
+ return Promise.all(childList.map((child) => {
671
+ return new Promise((resolve) => {
672
+ function deal() {
673
+ idle(
674
+ (deadline) => {
675
+ if (deadline instanceof IdleDeadline) {
676
+ if (deadline.timeRemaining()) {
677
+ callback(child, resolve);
678
+ } else {
679
+ deal();
680
+ }
681
+ } else {
682
+ callback(child, resolve);
683
+ }
684
+ },
685
+ {
686
+ fast
687
+ }
688
+ );
689
+ }
690
+ deal();
691
+ });
692
+ }));
693
+ }
661
694
  function addNotSlottedRightmost(sel) {
662
695
  sel = sel.trim();
663
696
  if (!sel) return sel;
@@ -779,10 +812,84 @@ function markSlottedSubtree(root) {
779
812
  root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
780
813
  }
781
814
  }
782
- function deepClone(node, sessionCache, options) {
815
+ async function getAccessibleIframeDocument(iframe, attempts = 3) {
816
+ const probe = () => {
817
+ try {
818
+ return iframe.contentDocument || iframe.contentWindow?.document || null;
819
+ } catch {
820
+ return null;
821
+ }
822
+ };
823
+ let doc = probe();
824
+ let i = 0;
825
+ while (i < attempts && (!doc || !doc.body && !doc.documentElement)) {
826
+ await new Promise((r) => setTimeout(r, 0));
827
+ doc = probe();
828
+ i++;
829
+ }
830
+ return doc && (doc.body || doc.documentElement) ? doc : null;
831
+ }
832
+ function measureContentBox(el) {
833
+ const rect = el.getBoundingClientRect();
834
+ let bl = 0, br = 0, bt = 0, bb = 0;
835
+ try {
836
+ const cs = getComputedStyle(el);
837
+ bl = parseFloat(cs.borderLeftWidth) || 0;
838
+ br = parseFloat(cs.borderRightWidth) || 0;
839
+ bt = parseFloat(cs.borderTopWidth) || 0;
840
+ bb = parseFloat(cs.borderBottomWidth) || 0;
841
+ } catch {
842
+ }
843
+ const contentWidth = Math.max(0, Math.round(rect.width - (bl + br)));
844
+ const contentHeight = Math.max(0, Math.round(rect.height - (bt + bb)));
845
+ return { contentWidth, contentHeight, rect };
846
+ }
847
+ function pinIframeViewport(doc, w, h) {
848
+ const style = doc.createElement("style");
849
+ style.setAttribute("data-sd-iframe-pin", "");
850
+ style.textContent = `html, body {margin: 0 !important;padding: 0 !important;width: ${w}px !important;height: ${h}px !important;min-width: ${w}px !important;min-height: ${h}px !important;box-sizing: border-box !important;overflow: hidden !important;background-clip: border-box !important;}`;
851
+ (doc.head || doc.documentElement).appendChild(style);
852
+ return () => {
853
+ try {
854
+ style.remove();
855
+ } catch {
856
+ }
857
+ };
858
+ }
859
+ async function rasterizeIframe(iframe, sessionCache, options) {
860
+ const doc = await getAccessibleIframeDocument(iframe, 3);
861
+ if (!doc) throw new Error("iframe document not accessible/ready");
862
+ const { contentWidth, contentHeight, rect } = measureContentBox(iframe);
863
+ const snap = options?.snap;
864
+ if (!snap || typeof snap.toPng !== "function") {
865
+ throw new Error("snapdom.toPng not available in iframe or window");
866
+ }
867
+ const nested = { ...options, scale: 1 };
868
+ const unpin = pinIframeViewport(doc, contentWidth, contentHeight);
869
+ let imgEl;
870
+ try {
871
+ imgEl = await snap.toPng(doc.documentElement, nested);
872
+ } finally {
873
+ unpin();
874
+ }
875
+ imgEl.style.display = "block";
876
+ imgEl.style.width = `${contentWidth}px`;
877
+ imgEl.style.height = `${contentHeight}px`;
878
+ const wrapper = document.createElement("div");
879
+ sessionCache.nodeMap.set(wrapper, iframe);
880
+ inlineAllStyles(iframe, wrapper, sessionCache, options);
881
+ wrapper.style.overflow = "hidden";
882
+ wrapper.style.display = "block";
883
+ if (!wrapper.style.width) wrapper.style.width = `${Math.round(rect.width)}px`;
884
+ if (!wrapper.style.height) wrapper.style.height = `${Math.round(rect.height)}px`;
885
+ wrapper.appendChild(imgEl);
886
+ return wrapper;
887
+ }
888
+ async function deepClone(node, sessionCache, options) {
783
889
  if (!node) throw new Error("Invalid node");
784
890
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
785
891
  let pendingSelectValue = null;
892
+ let pendingTextAreaValue = null;
786
893
  if (node.nodeType === Node.ELEMENT_NODE) {
787
894
  const tag = (node.localName || node.tagName || "").toLowerCase();
788
895
  if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
@@ -831,6 +938,20 @@ function deepClone(node, sessionCache, options) {
831
938
  }
832
939
  }
833
940
  if (node.tagName === "IFRAME") {
941
+ let sameOrigin = false;
942
+ try {
943
+ sameOrigin = !!(node.contentDocument || node.contentWindow?.document);
944
+ } catch {
945
+ sameOrigin = false;
946
+ }
947
+ if (sameOrigin) {
948
+ try {
949
+ const wrapper = await rasterizeIframe(node, sessionCache, options);
950
+ return wrapper;
951
+ } catch (err) {
952
+ console.warn("[SnapDOM] iframe rasterization failed, fallback:", err);
953
+ }
954
+ }
834
955
  if (options.placeholders) {
835
956
  const fallback = document.createElement("div");
836
957
  fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
@@ -870,14 +991,31 @@ function deepClone(node, sessionCache, options) {
870
991
  sessionCache.nodeMap.set(clone, node);
871
992
  if (node.tagName === "IMG") {
872
993
  freezeImgSrcset(node, clone);
994
+ try {
995
+ const rect = node.getBoundingClientRect();
996
+ let w = Math.round(rect.width || 0);
997
+ let h = Math.round(rect.height || 0);
998
+ if (!w || !h) {
999
+ const computed = window.getComputedStyle(node);
1000
+ const cssW = parseFloat(computed.width) || 0;
1001
+ const cssH = parseFloat(computed.height) || 0;
1002
+ const attrW = parseInt(node.getAttribute("width") || "", 10) || 0;
1003
+ const attrH = parseInt(node.getAttribute("height") || "", 10) || 0;
1004
+ const propW = node.width || node.naturalWidth || 0;
1005
+ const propH = node.height || node.naturalHeight || 0;
1006
+ w = Math.round(w || cssW || attrW || propW || 0);
1007
+ h = Math.round(h || cssH || attrH || propH || 0);
1008
+ }
1009
+ if (w) clone.dataset.snapdomWidth = String(w);
1010
+ if (h) clone.dataset.snapdomHeight = String(h);
1011
+ } catch {
1012
+ }
873
1013
  }
874
1014
  } catch (err) {
875
1015
  console.error("[Snapdom] Failed to clone node:", node, err);
876
1016
  throw err;
877
1017
  }
878
1018
  if (node instanceof HTMLTextAreaElement) {
879
- clone.textContent = node.value;
880
- clone.value = node.value;
881
1019
  const rect = node.getBoundingClientRect();
882
1020
  clone.style.width = `${rect.width}px`;
883
1021
  clone.style.height = `${rect.height}px`;
@@ -894,8 +1032,22 @@ function deepClone(node, sessionCache, options) {
894
1032
  if (node instanceof HTMLSelectElement) {
895
1033
  pendingSelectValue = node.value;
896
1034
  }
1035
+ if (node instanceof HTMLTextAreaElement) {
1036
+ pendingTextAreaValue = node.value;
1037
+ }
897
1038
  inlineAllStyles(node, clone, sessionCache, options);
898
1039
  if (node.shadowRoot) {
1040
+ let callback2 = function(child, resolve) {
1041
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
1042
+ return resolve(null);
1043
+ } else {
1044
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1045
+ resolve(clonedChild || null);
1046
+ }).catch((e) => {
1047
+ resolve(null);
1048
+ });
1049
+ }
1050
+ };
899
1051
  try {
900
1052
  const slots = node.shadowRoot.querySelectorAll("slot");
901
1053
  for (const s of slots) {
@@ -921,31 +1073,38 @@ function deepClone(node, sessionCache, options) {
921
1073
  const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
922
1074
  injectScopedStyle(clone, seed + rewritten, scopeId);
923
1075
  const shadowFrag = document.createDocumentFragment();
924
- for (const child of node.shadowRoot.childNodes) {
925
- if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") continue;
926
- const clonedChild = deepClone(child, sessionCache, options);
927
- if (clonedChild) shadowFrag.appendChild(clonedChild);
928
- }
1076
+ const cloneList2 = await idleCallback(Array.from(node.shadowRoot.childNodes), callback2, options.fast);
1077
+ shadowFrag.append(...cloneList2.filter((clonedChild) => !!clonedChild));
929
1078
  clone.appendChild(shadowFrag);
930
1079
  }
931
1080
  if (node.tagName === "SLOT") {
1081
+ let callback2 = function(child, resolve) {
1082
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1083
+ if (clonedChild) {
1084
+ markSlottedSubtree(clonedChild);
1085
+ }
1086
+ resolve(clonedChild || null);
1087
+ }).catch((e) => {
1088
+ resolve(null);
1089
+ });
1090
+ };
932
1091
  const assigned = node.assignedNodes?.({ flatten: true }) || [];
933
1092
  const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
934
1093
  const fragment = document.createDocumentFragment();
935
- for (const child of nodesToClone) {
936
- const clonedChild = deepClone(child, sessionCache, options);
937
- if (clonedChild) {
938
- markSlottedSubtree(clonedChild);
939
- fragment.appendChild(clonedChild);
940
- }
941
- }
1094
+ const cloneList2 = await idleCallback(Array.from(nodesToClone), callback2, options.fast);
1095
+ fragment.append(...cloneList2.filter((clonedChild) => !!clonedChild));
942
1096
  return fragment;
943
1097
  }
944
- for (const child of node.childNodes) {
945
- if (clonedAssignedNodes.has(child)) continue;
946
- const clonedChild = deepClone(child, sessionCache, options);
947
- if (clonedChild) clone.appendChild(clonedChild);
1098
+ function callback(child, resolve) {
1099
+ if (clonedAssignedNodes.has(child)) return resolve(null);
1100
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1101
+ resolve(clonedChild || null);
1102
+ }).catch((e) => {
1103
+ resolve(null);
1104
+ });
948
1105
  }
1106
+ const cloneList = await idleCallback(Array.from(node.childNodes), callback, options.fast);
1107
+ clone.append(...cloneList.filter((clonedChild) => !!clonedChild));
949
1108
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
950
1109
  clone.value = pendingSelectValue;
951
1110
  for (const opt of clone.options) {
@@ -956,6 +1115,9 @@ function deepClone(node, sessionCache, options) {
956
1115
  }
957
1116
  }
958
1117
  }
1118
+ if (pendingTextAreaValue !== null && clone instanceof HTMLTextAreaElement) {
1119
+ clone.textContent = pendingTextAreaValue;
1120
+ }
959
1121
  return clone;
960
1122
  }
961
1123
 
@@ -1826,7 +1988,7 @@ async function prepareClone(element, options = {}) {
1826
1988
  console.warn("inlineExternal defs or symbol failed:", e);
1827
1989
  }
1828
1990
  try {
1829
- clone = deepClone(element, sessionCache, options, element);
1991
+ clone = await deepClone(element, sessionCache, options, element);
1830
1992
  } catch (e) {
1831
1993
  console.warn("deepClone failed:", e);
1832
1994
  throw e;
@@ -2064,6 +2226,30 @@ async function inlineImages(clone, options = {}) {
2064
2226
  if (!img.height) img.height = img.naturalHeight || 100;
2065
2227
  return;
2066
2228
  }
2229
+ const { defaultImageUrl } = options || {};
2230
+ if (defaultImageUrl) {
2231
+ try {
2232
+ const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
2233
+ const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
2234
+ const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
2235
+ const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
2236
+ const styleW = parseFloat(img.style?.width || "") || 0;
2237
+ const styleH = parseFloat(img.style?.height || "") || 0;
2238
+ const width = dsW || styleW || attrW || img.width || void 0;
2239
+ const height = dsH || styleH || attrH || img.height || void 0;
2240
+ const fallbackUrl = typeof defaultImageUrl === "function" ? await defaultImageUrl({ width, height, src, element: img }) : defaultImageUrl;
2241
+ if (fallbackUrl) {
2242
+ const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
2243
+ img.src = fallbackData.data;
2244
+ if (!img.width && width) img.width = width;
2245
+ if (!img.height && height) img.height = height;
2246
+ if (!img.width) img.width = img.naturalWidth || 100;
2247
+ if (!img.height) img.height = img.naturalHeight || 100;
2248
+ return;
2249
+ }
2250
+ } catch {
2251
+ }
2252
+ }
2067
2253
  const w = img.width || img.naturalWidth || 100;
2068
2254
  const h = img.height || img.naturalHeight || 100;
2069
2255
  if (options.placeholders !== false) {
@@ -2096,17 +2282,40 @@ async function inlineImages(clone, options = {}) {
2096
2282
  // src/modules/background.js
2097
2283
  async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2098
2284
  const queue = [[source, clone]];
2099
- const imageProps = [
2285
+ const URL_PROPS = [
2100
2286
  "background-image",
2287
+ // Mask shorthands & images (both standard and WebKit)
2101
2288
  "mask",
2102
2289
  "mask-image",
2290
+ "-webkit-mask",
2103
2291
  "-webkit-mask-image",
2292
+ // Mask sources (rare, but keep)
2104
2293
  "mask-source",
2105
2294
  "mask-box-image-source",
2106
2295
  "mask-border-source",
2107
2296
  "-webkit-mask-box-image-source",
2297
+ // Border image
2108
2298
  "border-image",
2109
- "border-image-source",
2299
+ "border-image-source"
2300
+ ];
2301
+ const MASK_LAYOUT_PROPS = [
2302
+ "mask-position",
2303
+ "mask-size",
2304
+ "mask-repeat",
2305
+ // WebKit variants
2306
+ "-webkit-mask-position",
2307
+ "-webkit-mask-size",
2308
+ "-webkit-mask-repeat",
2309
+ // Extra (optional but helpful across engines)
2310
+ "mask-origin",
2311
+ "mask-clip",
2312
+ "-webkit-mask-origin",
2313
+ "-webkit-mask-clip",
2314
+ // Some engines expose X/Y position separately:
2315
+ "-webkit-mask-position-x",
2316
+ "-webkit-mask-position-y"
2317
+ ];
2318
+ const BORDER_AUX_PROPS = [
2110
2319
  "border-image-slice",
2111
2320
  "border-image-width",
2112
2321
  "border-image-outset",
@@ -2121,10 +2330,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2121
2330
  const bis = style.getPropertyValue("border-image-source");
2122
2331
  return bi && bi !== "none" || bis && bis !== "none";
2123
2332
  })();
2124
- for (const prop of imageProps) {
2125
- if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
2126
- continue;
2127
- }
2333
+ for (const prop of URL_PROPS) {
2128
2334
  const val = style.getPropertyValue(prop);
2129
2335
  if (!val || val === "none") continue;
2130
2336
  const splits = splitBackgroundImage(val);
@@ -2135,6 +2341,18 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2135
2341
  cloneNode.style.setProperty(prop, inlined.join(", "));
2136
2342
  }
2137
2343
  }
2344
+ for (const prop of MASK_LAYOUT_PROPS) {
2345
+ const val = style.getPropertyValue(prop);
2346
+ if (!val || val === "initial") continue;
2347
+ cloneNode.style.setProperty(prop, val);
2348
+ }
2349
+ if (hasBorderImage) {
2350
+ for (const prop of BORDER_AUX_PROPS) {
2351
+ const val = style.getPropertyValue(prop);
2352
+ if (!val || val === "initial") continue;
2353
+ cloneNode.style.setProperty(prop, val);
2354
+ }
2355
+ }
2138
2356
  const sChildren = Array.from(srcNode.children);
2139
2357
  const cChildren = Array.from(cloneNode.children);
2140
2358
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
@@ -2177,7 +2395,13 @@ async function captureDOM(element, options) {
2177
2395
  );
2178
2396
  await ensureFontsReady(families, 2);
2179
2397
  }
2180
- fontsCSS = await embedCustomFonts({ required, usedCodepoints, preCached: false, exclude: options.excludeFonts, useProxy: options.useProxy });
2398
+ fontsCSS = await embedCustomFonts({
2399
+ required,
2400
+ usedCodepoints,
2401
+ preCached: false,
2402
+ exclude: options.excludeFonts,
2403
+ useProxy: options.useProxy
2404
+ });
2181
2405
  resolve();
2182
2406
  }, { fast });
2183
2407
  });
@@ -2197,12 +2421,13 @@ async function captureDOM(element, options) {
2197
2421
  }
2198
2422
  await new Promise((resolve) => {
2199
2423
  idle(() => {
2200
- const rect = element.getBoundingClientRect();
2424
+ const rect = getNaturalBorderBoxSize(element);
2201
2425
  let w = rect.width;
2202
2426
  let h = rect.height;
2203
2427
  const hasW = Number.isFinite(options.width);
2204
2428
  const hasH = Number.isFinite(options.height);
2205
- const hasScale = typeof scale === "number" && scale !== 1;
2429
+ const scaleOpt = options.scale ?? 1;
2430
+ const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2206
2431
  if (!hasScale) {
2207
2432
  const aspect = rect.width / rect.height;
2208
2433
  if (hasW && hasH) {
@@ -2216,30 +2441,79 @@ async function captureDOM(element, options) {
2216
2441
  w = h * aspect;
2217
2442
  }
2218
2443
  }
2219
- w = Math.ceil(w);
2220
- h = Math.ceil(h);
2221
- clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2222
- clone.style.transformOrigin = "top left";
2223
- if (!hasScale && (hasW || hasH)) {
2224
- const originalW = rect.width;
2225
- const originalH = rect.height;
2226
- const scaleX = w / originalW;
2227
- const scaleY = h / originalH;
2228
- const existingTransform = clone.style.transform || "";
2229
- const scaleTransform = `scale(${scaleX}, ${scaleY})`;
2230
- clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
2444
+ w = Math.max(1, Math.ceil(w));
2445
+ h = Math.max(1, Math.ceil(h));
2446
+ const csEl = getComputedStyle(element);
2447
+ const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2448
+ const ind = readIndividualTransforms(element);
2449
+ let outW, outH, tx, ty, cancelTranslateX = 0, cancelTranslateY = 0;
2450
+ if (!hasBBoxAffectingTransform(element)) {
2451
+ const r = element.getBoundingClientRect();
2452
+ const vp = inflateRect(r, { top: 0, right: 0, bottom: 0, left: 0 });
2453
+ outW = Math.max(1, vp.width);
2454
+ outH = Math.max(1, vp.height);
2455
+ tx = -(vp.x - Math.floor(r.left));
2456
+ ty = -(vp.y - Math.floor(r.top));
2457
+ } else {
2458
+ const TOTAL = readTotalTransformMatrix({
2459
+ baseTransform,
2460
+ rotate: ind.rotate || "0deg",
2461
+ scale: ind.scale,
2462
+ translate: ind.translate
2463
+ });
2464
+ const e = TOTAL.e ?? TOTAL.m41 ?? 0;
2465
+ const f = TOTAL.f ?? TOTAL.m42 ?? 0;
2466
+ cancelTranslateX = -e;
2467
+ cancelTranslateY = -f;
2468
+ const M2D = TOTAL.is2D ? matrix2DNoTranslate(TOTAL) : matrix2DNoTranslate(new DOMMatrix(TOTAL.toString()));
2469
+ const bb = bboxFromMatrix(w, h, M2D);
2470
+ outW = Math.max(1, Math.ceil(bb.width));
2471
+ outH = Math.max(1, Math.ceil(bb.height));
2472
+ tx = -bb.minX;
2473
+ ty = -bb.minY;
2231
2474
  }
2232
2475
  const svgNS = "http://www.w3.org/2000/svg";
2233
2476
  const fo = document.createElementNS(svgNS, "foreignObject");
2234
2477
  fo.setAttribute("width", "100%");
2235
2478
  fo.setAttribute("height", "100%");
2479
+ fo.setAttribute("x", String(tx));
2480
+ fo.setAttribute("y", String(ty));
2481
+ fo.style.overflow = "visible";
2236
2482
  const styleTag = document.createElement("style");
2237
- styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
2483
+ styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
2238
2484
  fo.appendChild(styleTag);
2239
- fo.appendChild(clone);
2485
+ const container = document.createElement("div");
2486
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2487
+ container.style.width = `${w}px`;
2488
+ container.style.height = `${h}px`;
2489
+ container.style.overflow = "visible";
2490
+ container.style.transformOrigin = "0 0";
2491
+ const cancels = [];
2492
+ if (cancelTranslateX || cancelTranslateY) {
2493
+ cancels.push(`translate(${cancelTranslateX}px, ${cancelTranslateY}px)`);
2494
+ }
2495
+ if (!hasScale && (hasW || hasH)) {
2496
+ const sxWH = rect.width ? w / rect.width : 1;
2497
+ const syWH = rect.height ? h / rect.height : 1;
2498
+ cancels.push(`scale(${sxWH}, ${syWH})`);
2499
+ } else if (hasScale) {
2500
+ cancels.push(`scale(${scaleOpt})`);
2501
+ }
2502
+ if (cancels.length) container.style.transform = cancels.join(" ");
2503
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2504
+ clone.style.transformOrigin = "0 0";
2505
+ clone.style.position = "static";
2506
+ clone.style.left = "0";
2507
+ clone.style.top = "0";
2508
+ if (baseTransform) clone.style.transform = baseTransform;
2509
+ clone.style.rotate = ind.rotate || "0deg";
2510
+ clone.style.scale = ind.scale || "1";
2511
+ clone.style.translate = ind.translate || "0 0";
2512
+ container.appendChild(clone);
2513
+ fo.appendChild(container);
2240
2514
  const serializer = new XMLSerializer();
2241
2515
  const foString = serializer.serializeToString(fo);
2242
- const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
2516
+ const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
2243
2517
  const svgFooter = "</svg>";
2244
2518
  svgString = svgHeader + foString + svgFooter;
2245
2519
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2250,11 +2524,131 @@ async function captureDOM(element, options) {
2250
2524
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2251
2525
  return dataURL;
2252
2526
  }
2527
+ function getNaturalBorderBoxSize(el) {
2528
+ const cs = getComputedStyle(el);
2529
+ const px = (v) => parseFloat(v) || 0;
2530
+ let w = px(cs.width);
2531
+ let h = px(cs.height);
2532
+ if (cs.boxSizing === "content-box") {
2533
+ w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
2534
+ h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
2535
+ }
2536
+ return { width: w, height: h };
2537
+ }
2538
+ function matrixFromComputed(el) {
2539
+ const tr = getComputedStyle(el).transform;
2540
+ if (!tr || tr === "none") return new DOMMatrix();
2541
+ try {
2542
+ return new DOMMatrix(tr);
2543
+ } catch {
2544
+ return new WebKitCSSMatrix(tr);
2545
+ }
2546
+ }
2547
+ function matrix2DNoTranslate(M) {
2548
+ return new DOMMatrix([M.a, M.b, M.c, M.d, 0, 0]);
2549
+ }
2550
+ function bboxFromMatrix(w, h, M2D) {
2551
+ const pts = [
2552
+ { x: 0, y: 0 },
2553
+ { x: w, y: 0 },
2554
+ { x: 0, y: h },
2555
+ { x: w, y: h }
2556
+ ];
2557
+ const t = pts.map((p) => ({
2558
+ x: M2D.a * p.x + M2D.c * p.y,
2559
+ y: M2D.b * p.x + M2D.d * p.y
2560
+ }));
2561
+ const minX = Math.min(...t.map((p) => p.x));
2562
+ const maxX = Math.max(...t.map((p) => p.x));
2563
+ const minY = Math.min(...t.map((p) => p.y));
2564
+ const maxY = Math.max(...t.map((p) => p.y));
2565
+ return { minX, minY, width: maxX - minX, height: maxY - minY };
2566
+ }
2567
+ function readIndividualTransforms(el) {
2568
+ const out = { rotate: "0deg", scale: null, translate: null };
2569
+ const map = el.computedStyleMap?.();
2570
+ if (map) {
2571
+ const rot = map.get("rotate");
2572
+ if (rot) {
2573
+ const ang = rot.angle ?? rot;
2574
+ if (ang && "unit" in ang) {
2575
+ out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2576
+ } else {
2577
+ out.rotate = String(rot);
2578
+ }
2579
+ }
2580
+ const sc = map.get("scale");
2581
+ if (sc && sc.length) {
2582
+ const sx = sc[0]?.value ?? 1;
2583
+ const sy = sc[1]?.value ?? sx;
2584
+ out.scale = `${sx} ${sy}`;
2585
+ }
2586
+ const tr = map.get("translate");
2587
+ if (tr && tr.length) {
2588
+ const tx = tr[0]?.value ?? 0;
2589
+ const ty = tr[1]?.value ?? 0;
2590
+ const ux = tr[0]?.unit ?? "px";
2591
+ const uy = tr[1]?.unit ?? "px";
2592
+ out.translate = `${tx}${ux} ${ty}${uy}`;
2593
+ }
2594
+ return out;
2595
+ }
2596
+ const cs = getComputedStyle(el);
2597
+ out.rotate = cs.rotate && cs.rotate !== "none" ? cs.rotate : "0deg";
2598
+ out.scale = cs.scale && cs.scale !== "none" ? cs.scale : null;
2599
+ out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
2600
+ return out;
2601
+ }
2602
+ function hasBBoxAffectingTransform(el) {
2603
+ const t = getComputedStyle(el).transform || "none";
2604
+ if (t === "none" || t === "matrix(1, 0, 0, 1, 0, 0)") return false;
2605
+ if (/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,/i.test(t)) return false;
2606
+ return true;
2607
+ }
2608
+ function inflateRect(r, p) {
2609
+ const x = Math.floor(r.left - p.left);
2610
+ const y = Math.floor(r.top - p.top);
2611
+ const w = Math.ceil(r.width + p.left + p.right);
2612
+ const h = Math.ceil(r.height + p.top + p.bottom);
2613
+ return { x, y, width: w, height: h };
2614
+ }
2615
+ var __measureHost = null;
2616
+ function getMeasureHost() {
2617
+ if (__measureHost) return __measureHost;
2618
+ const n = document.createElement("div");
2619
+ n.id = "snapdom-measure-slot";
2620
+ n.setAttribute("aria-hidden", "true");
2621
+ Object.assign(n.style, {
2622
+ position: "absolute",
2623
+ left: "-99999px",
2624
+ top: "0px",
2625
+ width: "0px",
2626
+ height: "0px",
2627
+ overflow: "hidden",
2628
+ opacity: "0",
2629
+ pointerEvents: "none",
2630
+ contain: "size layout style"
2631
+ });
2632
+ document.documentElement.appendChild(n);
2633
+ __measureHost = n;
2634
+ return n;
2635
+ }
2636
+ function readTotalTransformMatrix(t) {
2637
+ const host = getMeasureHost();
2638
+ const tmp = document.createElement("div");
2639
+ tmp.style.transformOrigin = "0 0";
2640
+ if (t.baseTransform) tmp.style.transform = t.baseTransform;
2641
+ if (t.rotate) tmp.style.rotate = t.rotate;
2642
+ if (t.scale) tmp.style.scale = t.scale;
2643
+ if (t.translate) tmp.style.translate = t.translate;
2644
+ host.appendChild(tmp);
2645
+ const M = matrixFromComputed(tmp);
2646
+ host.removeChild(tmp);
2647
+ return M;
2648
+ }
2253
2649
 
2254
2650
  // src/core/context.js
2255
2651
  function normalizeCachePolicy(v) {
2256
- if (v === true) return "soft";
2257
- if (v === false) return "disabled";
2258
2652
  if (typeof v === "string") {
2259
2653
  const s = v.toLowerCase().trim();
2260
2654
  if (s === "disabled" || s === "full" || s === "auto" || s === "soft") return (
@@ -2282,6 +2676,7 @@ function createContext(options = {}) {
2282
2676
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2283
2677
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2284
2678
  excludeFonts: options.excludeFonts ?? void 0,
2679
+ defaultImageUrl: options.defaultImageUrl ?? void 0,
2285
2680
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2286
2681
  /** @type {CachePolicy} */
2287
2682
  cache: cachePolicy,
@@ -2315,7 +2710,7 @@ async function toImg(url, options) {
2315
2710
 
2316
2711
  // src/exporters/toCanvas.js
2317
2712
  async function toCanvas(url, options) {
2318
- const { scale: scale2, dpr } = options;
2713
+ const { scale, dpr } = options;
2319
2714
  const safari = isSafari();
2320
2715
  const img = new Image();
2321
2716
  img.crossOrigin = "anonymous";
@@ -2334,8 +2729,8 @@ async function toCanvas(url, options) {
2334
2729
  if (safari) {
2335
2730
  await new Promise((r) => setTimeout(r, 100));
2336
2731
  }
2337
- const width = img.naturalWidth * scale2;
2338
- const height = img.naturalHeight * scale2;
2732
+ const width = img.naturalWidth * scale;
2733
+ const height = img.naturalHeight * scale;
2339
2734
  const canvas = document.createElement("canvas");
2340
2735
  canvas.width = Math.ceil(width * dpr);
2341
2736
  canvas.height = Math.ceil(height * dpr);
@@ -2409,15 +2804,27 @@ async function snapdom(element, userOptions) {
2409
2804
  if (!element) throw new Error("Element cannot be null or undefined");
2410
2805
  const context = createContext(userOptions);
2411
2806
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2807
+ if (!context.snap) {
2808
+ context.snap = {
2809
+ toPng: (el, opts) => snapdom.toPng(el, opts),
2810
+ toImg: (el, opts) => snapdom.toImg(el, opts)
2811
+ };
2812
+ }
2412
2813
  return snapdom.capture(element, context, INTERNAL_TOKEN);
2413
2814
  }
2414
2815
  snapdom.capture = async (el, context, _token) => {
2415
- if (_token !== INTERNAL_TOKEN) {
2416
- throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
2417
- }
2816
+ if (_token !== INTERNAL_TOKEN) throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
2418
2817
  const url = await captureDOM(el, context);
2419
2818
  const ensureContext = (opts) => ({ ...context, ...opts || {} });
2420
- const withFormat = (format) => (opts) => rasterize(url, ensureContext({ ...opts || {}, format }));
2819
+ const withFormat = (format) => (opts) => {
2820
+ const next = ensureContext({ ...opts || {}, format });
2821
+ const wantsJpeg = format === "jpeg" || format === "jpg";
2822
+ const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
2823
+ if (wantsJpeg && noBg) {
2824
+ next.backgroundColor = "#ffffff";
2825
+ }
2826
+ return rasterize(url, next);
2827
+ };
2421
2828
  return {
2422
2829
  url,
2423
2830
  toRaw: () => url,