@zumer/snapdom 1.9.10-dev.0 → 1.9.10-dev.2

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.0
4
+ * v.1.9.10-dev.2
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: {
@@ -297,7 +300,6 @@ var NO_DEFAULTS_TAGS = /* @__PURE__ */ new Set([
297
300
  "script",
298
301
  "template",
299
302
  // SVG whole namespace (safe for LeaderLine/presentation attrs)
300
- "svg",
301
303
  "g",
302
304
  "defs",
303
305
  "use",
@@ -638,6 +640,11 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
638
640
  const ctx = _resolveCtx(sessionOrCtx, opts);
639
641
  const resetMode = ctx.options && ctx.options.cache || "auto";
640
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
+ }
641
648
  if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
642
649
  const author = source.getAttribute?.("style");
643
650
  if (author) clone.setAttribute("style", author);
@@ -659,6 +666,31 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
659
666
  }
660
667
 
661
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
+ }
662
694
  function addNotSlottedRightmost(sel) {
663
695
  sel = sel.trim();
664
696
  if (!sel) return sel;
@@ -780,10 +812,84 @@ function markSlottedSubtree(root) {
780
812
  root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
781
813
  }
782
814
  }
783
- 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) {
784
889
  if (!node) throw new Error("Invalid node");
785
890
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
786
891
  let pendingSelectValue = null;
892
+ let pendingTextAreaValue = null;
787
893
  if (node.nodeType === Node.ELEMENT_NODE) {
788
894
  const tag = (node.localName || node.tagName || "").toLowerCase();
789
895
  if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
@@ -832,9 +938,32 @@ function deepClone(node, sessionCache, options) {
832
938
  }
833
939
  }
834
940
  if (node.tagName === "IFRAME") {
835
- const fallback = document.createElement("div");
836
- 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;`;
837
- return fallback;
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
+ }
955
+ if (options.placeholders) {
956
+ const fallback = document.createElement("div");
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;`;
958
+ inlineAllStyles(node, fallback, sessionCache, options);
959
+ return fallback;
960
+ } else {
961
+ const rect = node.getBoundingClientRect();
962
+ const spacer = document.createElement("div");
963
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
964
+ inlineAllStyles(node, spacer, sessionCache, options);
965
+ return spacer;
966
+ }
838
967
  }
839
968
  if (node.getAttribute("data-capture") === "placeholder") {
840
969
  const clone2 = node.cloneNode(false);
@@ -862,18 +991,34 @@ function deepClone(node, sessionCache, options) {
862
991
  sessionCache.nodeMap.set(clone, node);
863
992
  if (node.tagName === "IMG") {
864
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
+ }
865
1013
  }
866
1014
  } catch (err) {
867
1015
  console.error("[Snapdom] Failed to clone node:", node, err);
868
1016
  throw err;
869
1017
  }
870
1018
  if (node instanceof HTMLTextAreaElement) {
871
- clone.textContent = node.value;
872
- clone.value = node.value;
873
1019
  const rect = node.getBoundingClientRect();
874
1020
  clone.style.width = `${rect.width}px`;
875
1021
  clone.style.height = `${rect.height}px`;
876
- return clone;
877
1022
  }
878
1023
  if (node instanceof HTMLInputElement) {
879
1024
  clone.value = node.value;
@@ -887,8 +1032,22 @@ function deepClone(node, sessionCache, options) {
887
1032
  if (node instanceof HTMLSelectElement) {
888
1033
  pendingSelectValue = node.value;
889
1034
  }
1035
+ if (node instanceof HTMLTextAreaElement) {
1036
+ pendingTextAreaValue = node.value;
1037
+ }
890
1038
  inlineAllStyles(node, clone, sessionCache, options);
891
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
+ };
892
1051
  try {
893
1052
  const slots = node.shadowRoot.querySelectorAll("slot");
894
1053
  for (const s of slots) {
@@ -914,31 +1073,38 @@ function deepClone(node, sessionCache, options) {
914
1073
  const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
915
1074
  injectScopedStyle(clone, seed + rewritten, scopeId);
916
1075
  const shadowFrag = document.createDocumentFragment();
917
- for (const child of node.shadowRoot.childNodes) {
918
- if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") continue;
919
- const clonedChild = deepClone(child, sessionCache, options);
920
- if (clonedChild) shadowFrag.appendChild(clonedChild);
921
- }
1076
+ const cloneList2 = await idleCallback(Array.from(node.shadowRoot.childNodes), callback2, options.fast);
1077
+ shadowFrag.append(...cloneList2.filter((clonedChild) => !!clonedChild));
922
1078
  clone.appendChild(shadowFrag);
923
1079
  }
924
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
+ };
925
1091
  const assigned = node.assignedNodes?.({ flatten: true }) || [];
926
1092
  const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
927
1093
  const fragment = document.createDocumentFragment();
928
- for (const child of nodesToClone) {
929
- const clonedChild = deepClone(child, sessionCache, options);
930
- if (clonedChild) {
931
- markSlottedSubtree(clonedChild);
932
- fragment.appendChild(clonedChild);
933
- }
934
- }
1094
+ const cloneList2 = await idleCallback(Array.from(nodesToClone), callback2, options.fast);
1095
+ fragment.append(...cloneList2.filter((clonedChild) => !!clonedChild));
935
1096
  return fragment;
936
1097
  }
937
- for (const child of node.childNodes) {
938
- if (clonedAssignedNodes.has(child)) continue;
939
- const clonedChild = deepClone(child, sessionCache, options);
940
- 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
+ });
941
1105
  }
1106
+ const cloneList = await idleCallback(Array.from(node.childNodes), callback, options.fast);
1107
+ clone.append(...cloneList.filter((clonedChild) => !!clonedChild));
942
1108
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
943
1109
  clone.value = pendingSelectValue;
944
1110
  for (const opt of clone.options) {
@@ -949,6 +1115,9 @@ function deepClone(node, sessionCache, options) {
949
1115
  }
950
1116
  }
951
1117
  }
1118
+ if (pendingTextAreaValue !== null && clone instanceof HTMLTextAreaElement) {
1119
+ clone.textContent = pendingTextAreaValue;
1120
+ }
952
1121
  return clone;
953
1122
  }
954
1123
 
@@ -1819,7 +1988,7 @@ async function prepareClone(element, options = {}) {
1819
1988
  console.warn("inlineExternal defs or symbol failed:", e);
1820
1989
  }
1821
1990
  try {
1822
- clone = deepClone(element, sessionCache, options, element);
1991
+ clone = await deepClone(element, sessionCache, options, element);
1823
1992
  } catch (e) {
1824
1993
  console.warn("deepClone failed:", e);
1825
1994
  throw e;
@@ -2057,22 +2226,52 @@ async function inlineImages(clone, options = {}) {
2057
2226
  if (!img.height) img.height = img.naturalHeight || 100;
2058
2227
  return;
2059
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
+ }
2060
2253
  const w = img.width || img.naturalWidth || 100;
2061
2254
  const h = img.height || img.naturalHeight || 100;
2062
- const fallback = document.createElement("div");
2063
- fallback.style.cssText = [
2064
- `width:${w}px`,
2065
- `height:${h}px`,
2066
- "background:#ccc",
2067
- "display:inline-block",
2068
- "text-align:center",
2069
- `line-height:${h}px`,
2070
- "color:#666",
2071
- "font-size:12px",
2072
- "overflow:hidden"
2073
- ].join(";");
2074
- fallback.textContent = "img";
2075
- img.replaceWith(fallback);
2255
+ if (options.placeholders !== false) {
2256
+ const fallback = document.createElement("div");
2257
+ fallback.style.cssText = [
2258
+ `width:${w}px`,
2259
+ `height:${h}px`,
2260
+ "background:#ccc",
2261
+ "display:inline-block",
2262
+ "text-align:center",
2263
+ `line-height:${h}px`,
2264
+ "color:#666",
2265
+ "font-size:12px",
2266
+ "overflow:hidden"
2267
+ ].join(";");
2268
+ fallback.textContent = "img";
2269
+ img.replaceWith(fallback);
2270
+ } else {
2271
+ const spacer = document.createElement("div");
2272
+ spacer.style.cssText = `display:inline-block;width:${w}px;height:${h}px;visibility:hidden;`;
2273
+ img.replaceWith(spacer);
2274
+ }
2076
2275
  };
2077
2276
  for (let i = 0; i < imgs.length; i += 4) {
2078
2277
  const group = imgs.slice(i, i + 4).map(processImg);
@@ -2164,7 +2363,13 @@ async function captureDOM(element, options) {
2164
2363
  );
2165
2364
  await ensureFontsReady(families, 2);
2166
2365
  }
2167
- fontsCSS = await embedCustomFonts({ required, usedCodepoints, preCached: false, exclude: options.excludeFonts, useProxy: options.useProxy });
2366
+ fontsCSS = await embedCustomFonts({
2367
+ required,
2368
+ usedCodepoints,
2369
+ preCached: false,
2370
+ exclude: options.excludeFonts,
2371
+ useProxy: options.useProxy
2372
+ });
2168
2373
  resolve();
2169
2374
  }, { fast });
2170
2375
  });
@@ -2184,12 +2389,13 @@ async function captureDOM(element, options) {
2184
2389
  }
2185
2390
  await new Promise((resolve) => {
2186
2391
  idle(() => {
2187
- const rect = element.getBoundingClientRect();
2392
+ const rect = getNaturalBorderBoxSize(element);
2188
2393
  let w = rect.width;
2189
2394
  let h = rect.height;
2190
2395
  const hasW = Number.isFinite(options.width);
2191
2396
  const hasH = Number.isFinite(options.height);
2192
- const hasScale = typeof scale === "number" && scale !== 1;
2397
+ const scaleOpt = options.scale ?? 1;
2398
+ const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2193
2399
  if (!hasScale) {
2194
2400
  const aspect = rect.width / rect.height;
2195
2401
  if (hasW && hasH) {
@@ -2203,30 +2409,79 @@ async function captureDOM(element, options) {
2203
2409
  w = h * aspect;
2204
2410
  }
2205
2411
  }
2206
- w = Math.ceil(w);
2207
- h = Math.ceil(h);
2208
- clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2209
- clone.style.transformOrigin = "top left";
2210
- if (!hasScale && (hasW || hasH)) {
2211
- const originalW = rect.width;
2212
- const originalH = rect.height;
2213
- const scaleX = w / originalW;
2214
- const scaleY = h / originalH;
2215
- const existingTransform = clone.style.transform || "";
2216
- const scaleTransform = `scale(${scaleX}, ${scaleY})`;
2217
- clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
2412
+ w = Math.max(1, Math.ceil(w));
2413
+ h = Math.max(1, Math.ceil(h));
2414
+ const csEl = getComputedStyle(element);
2415
+ const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2416
+ const ind = readIndividualTransforms(element);
2417
+ let outW, outH, tx, ty, cancelTranslateX = 0, cancelTranslateY = 0;
2418
+ if (!hasBBoxAffectingTransform(element)) {
2419
+ const r = element.getBoundingClientRect();
2420
+ const vp = inflateRect(r, { top: 0, right: 0, bottom: 0, left: 0 });
2421
+ outW = Math.max(1, vp.width);
2422
+ outH = Math.max(1, vp.height);
2423
+ tx = -(vp.x - Math.floor(r.left));
2424
+ ty = -(vp.y - Math.floor(r.top));
2425
+ } else {
2426
+ const TOTAL = readTotalTransformMatrix({
2427
+ baseTransform,
2428
+ rotate: ind.rotate || "0deg",
2429
+ scale: ind.scale,
2430
+ translate: ind.translate
2431
+ });
2432
+ const e = TOTAL.e ?? TOTAL.m41 ?? 0;
2433
+ const f = TOTAL.f ?? TOTAL.m42 ?? 0;
2434
+ cancelTranslateX = -e;
2435
+ cancelTranslateY = -f;
2436
+ const M2D = TOTAL.is2D ? matrix2DNoTranslate(TOTAL) : matrix2DNoTranslate(new DOMMatrix(TOTAL.toString()));
2437
+ const bb = bboxFromMatrix(w, h, M2D);
2438
+ outW = Math.max(1, Math.ceil(bb.width));
2439
+ outH = Math.max(1, Math.ceil(bb.height));
2440
+ tx = -bb.minX;
2441
+ ty = -bb.minY;
2218
2442
  }
2219
2443
  const svgNS = "http://www.w3.org/2000/svg";
2220
2444
  const fo = document.createElementNS(svgNS, "foreignObject");
2221
2445
  fo.setAttribute("width", "100%");
2222
2446
  fo.setAttribute("height", "100%");
2447
+ fo.setAttribute("x", String(tx));
2448
+ fo.setAttribute("y", String(ty));
2449
+ fo.style.overflow = "visible";
2223
2450
  const styleTag = document.createElement("style");
2224
- styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
2451
+ styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
2225
2452
  fo.appendChild(styleTag);
2226
- fo.appendChild(clone);
2453
+ const container = document.createElement("div");
2454
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2455
+ container.style.width = `${w}px`;
2456
+ container.style.height = `${h}px`;
2457
+ container.style.overflow = "visible";
2458
+ container.style.transformOrigin = "0 0";
2459
+ const cancels = [];
2460
+ if (cancelTranslateX || cancelTranslateY) {
2461
+ cancels.push(`translate(${cancelTranslateX}px, ${cancelTranslateY}px)`);
2462
+ }
2463
+ if (!hasScale && (hasW || hasH)) {
2464
+ const sxWH = rect.width ? w / rect.width : 1;
2465
+ const syWH = rect.height ? h / rect.height : 1;
2466
+ cancels.push(`scale(${sxWH}, ${syWH})`);
2467
+ } else if (hasScale) {
2468
+ cancels.push(`scale(${scaleOpt})`);
2469
+ }
2470
+ if (cancels.length) container.style.transform = cancels.join(" ");
2471
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2472
+ clone.style.transformOrigin = "0 0";
2473
+ clone.style.position = "static";
2474
+ clone.style.left = "0";
2475
+ clone.style.top = "0";
2476
+ if (baseTransform) clone.style.transform = baseTransform;
2477
+ clone.style.rotate = ind.rotate || "0deg";
2478
+ clone.style.scale = ind.scale || "1";
2479
+ clone.style.translate = ind.translate || "0 0";
2480
+ container.appendChild(clone);
2481
+ fo.appendChild(container);
2227
2482
  const serializer = new XMLSerializer();
2228
2483
  const foString = serializer.serializeToString(fo);
2229
- const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
2484
+ const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
2230
2485
  const svgFooter = "</svg>";
2231
2486
  svgString = svgHeader + foString + svgFooter;
2232
2487
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2237,6 +2492,128 @@ async function captureDOM(element, options) {
2237
2492
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2238
2493
  return dataURL;
2239
2494
  }
2495
+ function getNaturalBorderBoxSize(el) {
2496
+ const cs = getComputedStyle(el);
2497
+ const px = (v) => parseFloat(v) || 0;
2498
+ let w = px(cs.width);
2499
+ let h = px(cs.height);
2500
+ if (cs.boxSizing === "content-box") {
2501
+ w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
2502
+ h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
2503
+ }
2504
+ return { width: w, height: h };
2505
+ }
2506
+ function matrixFromComputed(el) {
2507
+ const tr = getComputedStyle(el).transform;
2508
+ if (!tr || tr === "none") return new DOMMatrix();
2509
+ try {
2510
+ return new DOMMatrix(tr);
2511
+ } catch {
2512
+ return new WebKitCSSMatrix(tr);
2513
+ }
2514
+ }
2515
+ function matrix2DNoTranslate(M) {
2516
+ return new DOMMatrix([M.a, M.b, M.c, M.d, 0, 0]);
2517
+ }
2518
+ function bboxFromMatrix(w, h, M2D) {
2519
+ const pts = [
2520
+ { x: 0, y: 0 },
2521
+ { x: w, y: 0 },
2522
+ { x: 0, y: h },
2523
+ { x: w, y: h }
2524
+ ];
2525
+ const t = pts.map((p) => ({
2526
+ x: M2D.a * p.x + M2D.c * p.y,
2527
+ y: M2D.b * p.x + M2D.d * p.y
2528
+ }));
2529
+ const minX = Math.min(...t.map((p) => p.x));
2530
+ const maxX = Math.max(...t.map((p) => p.x));
2531
+ const minY = Math.min(...t.map((p) => p.y));
2532
+ const maxY = Math.max(...t.map((p) => p.y));
2533
+ return { minX, minY, width: maxX - minX, height: maxY - minY };
2534
+ }
2535
+ function readIndividualTransforms(el) {
2536
+ const out = { rotate: "0deg", scale: null, translate: null };
2537
+ const map = el.computedStyleMap?.();
2538
+ if (map) {
2539
+ const rot = map.get("rotate");
2540
+ if (rot) {
2541
+ const ang = rot.angle ?? rot;
2542
+ if (ang && "unit" in ang) {
2543
+ out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2544
+ } else {
2545
+ out.rotate = String(rot);
2546
+ }
2547
+ }
2548
+ const sc = map.get("scale");
2549
+ if (sc && sc.length) {
2550
+ const sx = sc[0]?.value ?? 1;
2551
+ const sy = sc[1]?.value ?? sx;
2552
+ out.scale = `${sx} ${sy}`;
2553
+ }
2554
+ const tr = map.get("translate");
2555
+ if (tr && tr.length) {
2556
+ const tx = tr[0]?.value ?? 0;
2557
+ const ty = tr[1]?.value ?? 0;
2558
+ const ux = tr[0]?.unit ?? "px";
2559
+ const uy = tr[1]?.unit ?? "px";
2560
+ out.translate = `${tx}${ux} ${ty}${uy}`;
2561
+ }
2562
+ return out;
2563
+ }
2564
+ const cs = getComputedStyle(el);
2565
+ out.rotate = cs.rotate && cs.rotate !== "none" ? cs.rotate : "0deg";
2566
+ out.scale = cs.scale && cs.scale !== "none" ? cs.scale : null;
2567
+ out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
2568
+ return out;
2569
+ }
2570
+ function hasBBoxAffectingTransform(el) {
2571
+ const t = getComputedStyle(el).transform || "none";
2572
+ if (t === "none" || t === "matrix(1, 0, 0, 1, 0, 0)") return false;
2573
+ if (/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,/i.test(t)) return false;
2574
+ return true;
2575
+ }
2576
+ function inflateRect(r, p) {
2577
+ const x = Math.floor(r.left - p.left);
2578
+ const y = Math.floor(r.top - p.top);
2579
+ const w = Math.ceil(r.width + p.left + p.right);
2580
+ const h = Math.ceil(r.height + p.top + p.bottom);
2581
+ return { x, y, width: w, height: h };
2582
+ }
2583
+ var __measureHost = null;
2584
+ function getMeasureHost() {
2585
+ if (__measureHost) return __measureHost;
2586
+ const n = document.createElement("div");
2587
+ n.id = "snapdom-measure-slot";
2588
+ n.setAttribute("aria-hidden", "true");
2589
+ Object.assign(n.style, {
2590
+ position: "absolute",
2591
+ left: "-99999px",
2592
+ top: "0px",
2593
+ width: "0px",
2594
+ height: "0px",
2595
+ overflow: "hidden",
2596
+ opacity: "0",
2597
+ pointerEvents: "none",
2598
+ contain: "size layout style"
2599
+ });
2600
+ document.documentElement.appendChild(n);
2601
+ __measureHost = n;
2602
+ return n;
2603
+ }
2604
+ function readTotalTransformMatrix(t) {
2605
+ const host = getMeasureHost();
2606
+ const tmp = document.createElement("div");
2607
+ tmp.style.transformOrigin = "0 0";
2608
+ if (t.baseTransform) tmp.style.transform = t.baseTransform;
2609
+ if (t.rotate) tmp.style.rotate = t.rotate;
2610
+ if (t.scale) tmp.style.scale = t.scale;
2611
+ if (t.translate) tmp.style.translate = t.translate;
2612
+ host.appendChild(tmp);
2613
+ const M = matrixFromComputed(tmp);
2614
+ host.removeChild(tmp);
2615
+ return M;
2616
+ }
2240
2617
 
2241
2618
  // src/core/context.js
2242
2619
  function normalizeCachePolicy(v) {
@@ -2262,11 +2639,14 @@ function createContext(options = {}) {
2262
2639
  // DOM filters
2263
2640
  exclude: options.exclude ?? [],
2264
2641
  filter: options.filter ?? null,
2642
+ placeholders: options.placeholders !== false,
2643
+ // default true
2265
2644
  // Fuentes
2266
2645
  embedFonts: options.embedFonts ?? false,
2267
2646
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2268
2647
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2269
2648
  excludeFonts: options.excludeFonts ?? void 0,
2649
+ defaultImageUrl: options.defaultImageUrl ?? void 0,
2270
2650
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2271
2651
  /** @type {CachePolicy} */
2272
2652
  cache: cachePolicy,
@@ -2300,31 +2680,41 @@ async function toImg(url, options) {
2300
2680
 
2301
2681
  // src/exporters/toCanvas.js
2302
2682
  async function toCanvas(url, options) {
2683
+ const { scale, dpr } = options;
2684
+ const safari = isSafari();
2303
2685
  const img = new Image();
2304
2686
  img.crossOrigin = "anonymous";
2305
2687
  img.loading = "eager";
2306
2688
  img.decoding = "sync";
2307
2689
  img.src = url;
2308
- const isSafariBrowser = isSafari();
2309
- let appended = false;
2310
- if (isSafariBrowser) {
2311
- document.body.appendChild(img);
2312
- appended = true;
2690
+ let tempSlot = null;
2691
+ if (safari) {
2692
+ tempSlot = document.createElement("div");
2693
+ tempSlot.setAttribute("aria-hidden", "true");
2694
+ tempSlot.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;contain:size layout style;";
2695
+ document.body.appendChild(tempSlot);
2696
+ tempSlot.appendChild(img);
2313
2697
  }
2314
2698
  await img.decode();
2315
- if (isSafariBrowser) await new Promise((resolve) => setTimeout(resolve, 100));
2316
- const width = img.naturalWidth * options.scale;
2317
- const height = img.naturalHeight * options.scale;
2699
+ if (safari) {
2700
+ await new Promise((r) => setTimeout(r, 100));
2701
+ }
2702
+ const width = img.naturalWidth * scale;
2703
+ const height = img.naturalHeight * scale;
2318
2704
  const canvas = document.createElement("canvas");
2319
- const dpr = options.dpr;
2320
2705
  canvas.width = Math.ceil(width * dpr);
2321
2706
  canvas.height = Math.ceil(height * dpr);
2322
2707
  canvas.style.width = `${width}px`;
2323
2708
  canvas.style.height = `${height}px`;
2324
2709
  const ctx = canvas.getContext("2d");
2325
- ctx.scale(dpr, dpr);
2710
+ if (dpr !== 1) ctx.scale(dpr, dpr);
2326
2711
  ctx.drawImage(img, 0, 0, width, height);
2327
- if (appended) img.remove();
2712
+ if (tempSlot && tempSlot.parentNode) {
2713
+ try {
2714
+ tempSlot.parentNode.removeChild(tempSlot);
2715
+ } catch {
2716
+ }
2717
+ }
2328
2718
  return canvas;
2329
2719
  }
2330
2720
 
@@ -2384,6 +2774,14 @@ async function snapdom(element, userOptions) {
2384
2774
  if (!element) throw new Error("Element cannot be null or undefined");
2385
2775
  const context = createContext(userOptions);
2386
2776
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2777
+ if (!context.snap) {
2778
+ context.snap = {
2779
+ /** Delegates to this module’s PNG path (tu build devuelve <img>). */
2780
+ toPng: (el, opts) => snapdom.toPng(el, opts),
2781
+ /** Útil si en algún flujo necesitás vector o <img> directo. */
2782
+ toImg: (el, opts) => snapdom.toImg(el, opts)
2783
+ };
2784
+ }
2387
2785
  return snapdom.capture(element, context, INTERNAL_TOKEN);
2388
2786
  }
2389
2787
  snapdom.capture = async (el, context, _token) => {