@zumer/snapdom 1.9.12-dev.0 → 1.9.12-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.12-dev.0
4
+ * v.1.9.12-dev.2
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -626,7 +626,10 @@ function idle(fn, { fast = false } = {}) {
626
626
  }
627
627
  }
628
628
  function isSafari() {
629
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
629
+ const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
630
+ const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
631
+ const isWeChatUA = /(MicroMessenger|wxwork|WeCom|WindowsWechat|MacWechat)/i.test(ua);
632
+ return isSafariUA || isWeChatUA;
630
633
  }
631
634
 
632
635
  // src/modules/styles.js
@@ -660,7 +663,7 @@ function setupInvalidationOnce(root = document.documentElement) {
660
663
  } catch {
661
664
  }
662
665
  }
663
- function snapshotComputedStyleFull(style) {
666
+ function snapshotComputedStyleFull(style, options = {}) {
664
667
  const out = {};
665
668
  const vis = style.getPropertyValue("visibility");
666
669
  for (let i = 0; i < style.length; i++) {
@@ -671,6 +674,24 @@ function snapshotComputedStyleFull(style) {
671
674
  }
672
675
  out[prop] = val;
673
676
  }
677
+ if (options.embedFonts) {
678
+ const EXTRA_FONT_PROPS = [
679
+ "font-feature-settings",
680
+ "font-variation-settings",
681
+ "font-kerning",
682
+ "font-variant",
683
+ "font-variant-ligatures",
684
+ "font-optical-sizing"
685
+ ];
686
+ for (const prop of EXTRA_FONT_PROPS) {
687
+ if (out[prop]) continue;
688
+ try {
689
+ const v = style.getPropertyValue(prop);
690
+ if (v) out[prop] = v;
691
+ } catch {
692
+ }
693
+ }
694
+ }
674
695
  if (vis === "hidden") out.opacity = "0";
675
696
  return out;
676
697
  }
@@ -683,11 +704,12 @@ function styleSignature(snap) {
683
704
  __snapshotSig.set(snap, sig);
684
705
  return sig;
685
706
  }
686
- function getSnapshot(el, preStyle = null) {
707
+ function getSnapshot(el, preStyle = null, options = {}) {
687
708
  const rec = snapshotCache.get(el);
688
709
  if (rec && rec.epoch === __epoch) return rec.snapshot;
689
710
  const style = preStyle || getComputedStyle(el);
690
- const snap = snapshotComputedStyleFull(style);
711
+ const snap = snapshotComputedStyleFull(style, options);
712
+ stripHeightForWrappers(el, style, snap);
691
713
  snapshotCache.set(el, { epoch: __epoch, snapshot: snap });
692
714
  return snap;
693
715
  }
@@ -741,7 +763,7 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
741
763
  session.styleCache.set(source, getComputedStyle(source));
742
764
  }
743
765
  const pre = session.styleCache.get(source);
744
- const snap = getSnapshot(source, pre);
766
+ const snap = getSnapshot(source, pre, ctx.options);
745
767
  const sig = styleSignature(snap);
746
768
  let key = persist.snapshotKeyCache.get(sig);
747
769
  if (!key) {
@@ -751,6 +773,49 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
751
773
  }
752
774
  session.styleMap.set(clone, key);
753
775
  }
776
+ function isReplaced(el) {
777
+ return el instanceof HTMLImageElement || el instanceof HTMLCanvasElement || el instanceof HTMLVideoElement || el instanceof HTMLIFrameElement || el instanceof SVGElement || el instanceof HTMLObjectElement || el instanceof HTMLEmbedElement;
778
+ }
779
+ function hasBox(cs) {
780
+ if (cs.backgroundImage && cs.backgroundImage !== "none") return true;
781
+ if (cs.backgroundColor && cs.backgroundColor !== "rgba(0, 0, 0, 0)" && cs.backgroundColor !== "transparent") return true;
782
+ if ((parseFloat(cs.borderTopWidth) || 0) > 0) return true;
783
+ if ((parseFloat(cs.borderBottomWidth) || 0) > 0) return true;
784
+ if ((parseFloat(cs.paddingTop) || 0) > 0) return true;
785
+ if ((parseFloat(cs.paddingBottom) || 0) > 0) return true;
786
+ const ob = cs.overflowBlock || cs.overflowY || "visible";
787
+ return ob !== "visible";
788
+ }
789
+ function isFlexOrGridItem(el) {
790
+ const p = el.parentElement;
791
+ if (!p) return false;
792
+ const pd = getComputedStyle(p).display || "";
793
+ return pd.includes("flex") || pd.includes("grid");
794
+ }
795
+ function hasFlowFast(el, cs) {
796
+ if (el.textContent && /\S/.test(el.textContent)) return true;
797
+ const f = el.firstElementChild, l = el.lastElementChild;
798
+ if (f && f.tagName === "BR" || l && l.tagName === "BR") return true;
799
+ const sh = el.scrollHeight;
800
+ if (sh === 0) return false;
801
+ const pt = parseFloat(cs.paddingTop) || 0;
802
+ const pb = parseFloat(cs.paddingBottom) || 0;
803
+ return sh > pt + pb;
804
+ }
805
+ function stripHeightForWrappers(el, cs, snap) {
806
+ if (el instanceof HTMLElement && el.style && el.style.height) return;
807
+ const disp = cs.display || "";
808
+ if (disp.includes("flex") || disp.includes("grid")) return;
809
+ if (isReplaced(el)) return;
810
+ const pos = cs.position;
811
+ if (pos === "absolute" || pos === "fixed" || pos === "sticky") return;
812
+ if (cs.transform !== "none") return;
813
+ if (hasBox(cs)) return;
814
+ if (isFlexOrGridItem(el)) return;
815
+ if (!hasFlowFast(el, cs)) return;
816
+ delete snap.height;
817
+ delete snap["block-size"];
818
+ }
754
819
 
755
820
  // src/core/clone.js
756
821
  function idleCallback(childList, callback, fast) {
@@ -985,7 +1050,7 @@ async function deepClone(node, sessionCache, options) {
985
1050
  return node.cloneNode(true);
986
1051
  }
987
1052
  if (node.getAttribute("data-capture") === "exclude") {
988
- if (options.excludeMode === "visuallyHide") {
1053
+ if (options.excludeMode === "hide") {
989
1054
  const spacer = document.createElement("div");
990
1055
  const rect = node.getBoundingClientRect();
991
1056
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -998,7 +1063,7 @@ async function deepClone(node, sessionCache, options) {
998
1063
  for (const selector of options.exclude) {
999
1064
  try {
1000
1065
  if (node.matches?.(selector)) {
1001
- if (options.excludeMode === "visuallyHide") {
1066
+ if (options.excludeMode === "hide") {
1002
1067
  const spacer = document.createElement("div");
1003
1068
  const rect = node.getBoundingClientRect();
1004
1069
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -1015,7 +1080,7 @@ async function deepClone(node, sessionCache, options) {
1015
1080
  if (typeof options.filter === "function") {
1016
1081
  try {
1017
1082
  if (!options.filter(node)) {
1018
- if (options.filterMode === "visuallyHide") {
1083
+ if (options.filterMode === "hide") {
1019
1084
  const spacer = document.createElement("div");
1020
1085
  const rect = node.getBoundingClientRect();
1021
1086
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -1898,7 +1963,182 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
1898
1963
  }
1899
1964
  }
1900
1965
 
1966
+ // src/modules/counter.js
1967
+ function hasCounters(input) {
1968
+ return /\bcounter\s*\(|\bcounters\s*\(/.test(input || "");
1969
+ }
1970
+ function unquoteDoubleStrings(s) {
1971
+ return (s || "").replace(/"([^"]*)"/g, "$1");
1972
+ }
1973
+ function alpha(n, upper = false) {
1974
+ let s = "", x = Math.max(1, n);
1975
+ while (x > 0) {
1976
+ x--;
1977
+ s = String.fromCharCode(97 + x % 26) + s;
1978
+ x = Math.floor(x / 26);
1979
+ }
1980
+ return upper ? s.toUpperCase() : s;
1981
+ }
1982
+ function roman(n, upper = true) {
1983
+ const map = [[1e3, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]];
1984
+ let num = Math.max(1, Math.min(3999, n)), out = "";
1985
+ for (const [v, sym] of map) while (num >= v) {
1986
+ out += sym;
1987
+ num -= v;
1988
+ }
1989
+ return upper ? out : out.toLowerCase();
1990
+ }
1991
+ function formatCounter(value, style) {
1992
+ switch ((style || "decimal").toLowerCase()) {
1993
+ case "decimal":
1994
+ return String(Math.max(0, value));
1995
+ case "decimal-leading-zero":
1996
+ return (value < 10 ? "0" : "") + String(Math.max(0, value));
1997
+ case "lower-alpha":
1998
+ return alpha(value, false);
1999
+ case "upper-alpha":
2000
+ return alpha(value, true);
2001
+ case "lower-roman":
2002
+ return roman(value, false);
2003
+ case "upper-roman":
2004
+ return roman(value, true);
2005
+ default:
2006
+ return String(Math.max(0, value));
2007
+ }
2008
+ }
2009
+ function buildCounterContext(root) {
2010
+ const nodeCounters = /* @__PURE__ */ new WeakMap();
2011
+ const rootEl = root instanceof Document ? root.documentElement : root;
2012
+ const isLi = (el) => el && el.tagName === "LI";
2013
+ const countPrevLi = (li) => {
2014
+ let c = 0, p = li?.parentElement;
2015
+ if (!p) return 0;
2016
+ for (const sib of p.children) {
2017
+ if (sib === li) break;
2018
+ if (sib.tagName === "LI") c++;
2019
+ }
2020
+ return c;
2021
+ };
2022
+ const cloneMap = (m) => {
2023
+ const out = /* @__PURE__ */ new Map();
2024
+ for (const [k, arr] of m) out.set(k, arr.slice());
2025
+ return out;
2026
+ };
2027
+ const applyTo = (baseMap, parentMap, el) => {
2028
+ const map = cloneMap(baseMap);
2029
+ let reset;
2030
+ try {
2031
+ reset = el.style?.counterReset || getComputedStyle(el).counterReset;
2032
+ } catch {
2033
+ }
2034
+ if (reset && reset !== "none") {
2035
+ for (const part of reset.split(",")) {
2036
+ const toks = part.trim().split(/\s+/);
2037
+ const name = toks[0];
2038
+ const val = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 0;
2039
+ if (!name) continue;
2040
+ const parentStack = parentMap.get(name);
2041
+ if (parentStack && parentStack.length) {
2042
+ const s = parentStack.slice();
2043
+ s.push(val);
2044
+ map.set(name, s);
2045
+ } else {
2046
+ map.set(name, [val]);
2047
+ }
2048
+ }
2049
+ }
2050
+ let inc;
2051
+ try {
2052
+ inc = el.style?.counterIncrement || getComputedStyle(el).counterIncrement;
2053
+ } catch {
2054
+ }
2055
+ if (inc && inc !== "none") {
2056
+ for (const part of inc.split(",")) {
2057
+ const toks = part.trim().split(/\s+/);
2058
+ const name = toks[0];
2059
+ const by = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 1;
2060
+ if (!name) continue;
2061
+ const stack = map.get(name) || [];
2062
+ if (stack.length === 0) stack.push(0);
2063
+ stack[stack.length - 1] += by;
2064
+ map.set(name, stack);
2065
+ }
2066
+ }
2067
+ try {
2068
+ const cs = getComputedStyle(el);
2069
+ if (cs.display === "list-item" && isLi(el)) {
2070
+ const p = el.parentElement;
2071
+ let idx = 1;
2072
+ if (p && p.tagName === "OL") {
2073
+ const startAttr = p.getAttribute("start");
2074
+ const start = Number.isFinite(Number(startAttr)) ? Number(startAttr) : 1;
2075
+ const prev = countPrevLi(el);
2076
+ const ownAttr = el.getAttribute("value");
2077
+ idx = Number.isFinite(Number(ownAttr)) ? Number(ownAttr) : start + prev;
2078
+ } else {
2079
+ idx = 1 + countPrevLi(el);
2080
+ }
2081
+ const s = map.get("list-item") || [];
2082
+ if (s.length === 0) s.push(0);
2083
+ s[s.length - 1] = idx;
2084
+ map.set("list-item", s);
2085
+ }
2086
+ } catch {
2087
+ }
2088
+ return map;
2089
+ };
2090
+ const build = (el, parentMap, carryMap) => {
2091
+ const curr = applyTo(carryMap, parentMap, el);
2092
+ nodeCounters.set(el, curr);
2093
+ let nextCarry = curr;
2094
+ for (const child of el.children) {
2095
+ const childCarry = build(child, curr, nextCarry);
2096
+ nextCarry = childCarry;
2097
+ }
2098
+ return curr;
2099
+ };
2100
+ const empty = /* @__PURE__ */ new Map();
2101
+ build(rootEl, empty, empty);
2102
+ return {
2103
+ get(node, name) {
2104
+ const s = nodeCounters.get(node)?.get(name);
2105
+ return s && s.length ? s[s.length - 1] : 0;
2106
+ },
2107
+ getStack(node, name) {
2108
+ const s = nodeCounters.get(node)?.get(name);
2109
+ return s ? s.slice() : [];
2110
+ }
2111
+ };
2112
+ }
2113
+ function resolveCountersInContent(raw, node, ctx) {
2114
+ if (!raw || raw === "none") return raw;
2115
+ try {
2116
+ const RX = /\b(counter|counters)\s*\(([^)]+)\)/g;
2117
+ let out = raw.replace(RX, (_, fn, args) => {
2118
+ const parts = String(args).split(",").map((s) => s.trim());
2119
+ if (fn === "counter") {
2120
+ const name = parts[0]?.replace(/^["']|["']$/g, "");
2121
+ const style = (parts[1] || "decimal").toLowerCase();
2122
+ const v = ctx.get(node, name);
2123
+ return formatCounter(v, style);
2124
+ } else {
2125
+ const name = parts[0]?.replace(/^["']|["']$/g, "");
2126
+ const sep = parts[1]?.replace(/^["']|["']$/g, "") ?? "";
2127
+ const style = (parts[2] || "decimal").toLowerCase();
2128
+ const stack = ctx.getStack(node, name);
2129
+ if (!stack.length) return "";
2130
+ const pieces = stack.map((v) => formatCounter(v, style));
2131
+ return pieces.join(sep);
2132
+ }
2133
+ });
2134
+ return unquoteDoubleStrings(out);
2135
+ } catch {
2136
+ return "- ";
2137
+ }
2138
+ }
2139
+
1901
2140
  // src/modules/pseudo.js
2141
+ var counterCtx = null;
1902
2142
  async function inlinePseudoElements(source, clone, sessionCache, options) {
1903
2143
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
1904
2144
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
@@ -1932,7 +2172,11 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
1932
2172
  continue;
1933
2173
  }
1934
2174
  const content = style.content;
1935
- const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
2175
+ let cleanContent = parseContent(content);
2176
+ if (hasCounters(content)) {
2177
+ counterCtx = buildCounterContext(source.ownerDocument || document);
2178
+ cleanContent = resolveCountersInContent(cleanContent, source, counterCtx);
2179
+ }
1936
2180
  const bg = style.backgroundImage;
1937
2181
  const bgColor = style.backgroundColor;
1938
2182
  const fontFamily = style.fontFamily;
@@ -1949,10 +2193,10 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
1949
2193
  const hasExplicitContent = content !== "none" && cleanContent !== "";
1950
2194
  const hasBg = bg && bg !== "none";
1951
2195
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
1952
- const hasBox = display !== "inline" && (width > 0 || height > 0);
2196
+ const hasBox2 = display !== "inline" && (width > 0 || height > 0);
1953
2197
  const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
1954
2198
  const hasTransform = transform && transform !== "none";
1955
- const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
2199
+ const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox2 || hasBorder || hasTransform;
1956
2200
  if (!shouldRender) continue;
1957
2201
  const pseudoEl = document.createElement("span");
1958
2202
  pseudoEl.dataset.snapdomPseudo = pseudo;
@@ -1994,7 +2238,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
1994
2238
  }
1995
2239
  if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
1996
2240
  const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
1997
- const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
2241
+ const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox2 || hasBorder || hasTransform;
1998
2242
  if (!hasVisibleBox) continue;
1999
2243
  if (pseudo === "::before") {
2000
2244
  clone.insertBefore(pseudoEl, clone.firstChild);
@@ -2317,8 +2561,8 @@ async function inlineImages(clone, options = {}) {
2317
2561
  if (!img.height) img.height = img.naturalHeight || 100;
2318
2562
  return;
2319
2563
  }
2320
- const { defaultImageUrl } = options || {};
2321
- if (defaultImageUrl) {
2564
+ const { fallbackURL } = options || {};
2565
+ if (fallbackURL) {
2322
2566
  try {
2323
2567
  const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
2324
2568
  const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
@@ -2326,14 +2570,14 @@ async function inlineImages(clone, options = {}) {
2326
2570
  const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
2327
2571
  const styleW = parseFloat(img.style?.width || "") || 0;
2328
2572
  const styleH = parseFloat(img.style?.height || "") || 0;
2329
- const width = dsW || styleW || attrW || img.width || void 0;
2330
- const height = dsH || styleH || attrH || img.height || void 0;
2331
- const fallbackUrl = typeof defaultImageUrl === "function" ? await defaultImageUrl({ width, height, src, element: img }) : defaultImageUrl;
2573
+ const fbW = dsW || styleW || attrW || img.width || void 0;
2574
+ const fbH = dsH || styleH || attrH || img.height || void 0;
2575
+ const fallbackUrl = typeof fallbackURL === "function" ? await fallbackURL({ width: fbW, height: fbH, src, element: img }) : fallbackURL;
2332
2576
  if (fallbackUrl) {
2333
2577
  const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
2334
2578
  img.src = fallbackData.data;
2335
- if (!img.width && width) img.width = width;
2336
- if (!img.height && height) img.height = height;
2579
+ if (!img.width && fbW) img.width = fbW;
2580
+ if (!img.height && fbH) img.height = fbH;
2337
2581
  if (!img.width) img.width = img.naturalWidth || 100;
2338
2582
  if (!img.height) img.height = img.naturalHeight || 100;
2339
2583
  return;
@@ -2452,6 +2696,63 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2452
2696
  }
2453
2697
  }
2454
2698
 
2699
+ // src/modules/lineClamp.js
2700
+ function lineClamp(el) {
2701
+ if (!el) return () => {
2702
+ };
2703
+ const lines = getClamp(el);
2704
+ if (lines <= 0) return () => {
2705
+ };
2706
+ if (!isPlainTextContainer(el)) return () => {
2707
+ };
2708
+ const cs = getComputedStyle(el);
2709
+ const targetH = Math.round(usedLineHeightPx(cs) * lines + vpad(cs));
2710
+ const original = el.textContent ?? "";
2711
+ const prevText = original;
2712
+ if (el.scrollHeight <= targetH + 0.5) {
2713
+ return () => {
2714
+ };
2715
+ }
2716
+ let lo = 0, hi = original.length, best = -1;
2717
+ while (lo <= hi) {
2718
+ const mid = lo + hi >> 1;
2719
+ el.textContent = original.slice(0, mid) + "\u2026";
2720
+ if (el.scrollHeight <= targetH + 0.5) {
2721
+ best = mid;
2722
+ lo = mid + 1;
2723
+ } else {
2724
+ hi = mid - 1;
2725
+ }
2726
+ }
2727
+ el.textContent = (best >= 0 ? original.slice(0, best) : "") + "\u2026";
2728
+ return () => {
2729
+ el.textContent = prevText;
2730
+ };
2731
+ }
2732
+ function getClamp(el) {
2733
+ const cs = getComputedStyle(el);
2734
+ let v = cs.getPropertyValue("-webkit-line-clamp") || cs.getPropertyValue("line-clamp");
2735
+ v = (v || "").trim();
2736
+ const n = parseInt(v, 10);
2737
+ return Number.isFinite(n) && n > 0 ? n : 0;
2738
+ }
2739
+ function usedLineHeightPx(cs) {
2740
+ const lh = (cs.lineHeight || "").trim();
2741
+ const fs = parseFloat(cs.fontSize) || 16;
2742
+ if (!lh || lh === "normal") return Math.round(fs * 1.2);
2743
+ if (lh.endsWith("px")) return parseFloat(lh);
2744
+ if (/^\d+(\.\d+)?$/.test(lh)) return Math.round(parseFloat(lh) * fs);
2745
+ if (lh.endsWith("%")) return Math.round(parseFloat(lh) / 100 * fs);
2746
+ return Math.round(fs * 1.2);
2747
+ }
2748
+ function vpad(cs) {
2749
+ return (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
2750
+ }
2751
+ function isPlainTextContainer(el) {
2752
+ if (el.childElementCount > 0) return false;
2753
+ return Array.from(el.childNodes).some((n) => n.nodeType === Node.TEXT_NODE);
2754
+ }
2755
+
2455
2756
  // src/core/capture.js
2456
2757
  async function captureDOM(element, options) {
2457
2758
  if (!element) throw new Error("Element cannot be null or undefined");
@@ -2462,7 +2763,12 @@ async function captureDOM(element, options) {
2462
2763
  let baseCSS = "";
2463
2764
  let dataURL;
2464
2765
  let svgString;
2465
- ({ clone, classCSS, styleCache } = await prepareClone(element, options));
2766
+ const undoClamp = lineClamp(element);
2767
+ try {
2768
+ ({ clone, classCSS, styleCache } = await prepareClone(element, options));
2769
+ } finally {
2770
+ undoClamp();
2771
+ }
2466
2772
  await new Promise((resolve) => {
2467
2773
  idle(async () => {
2468
2774
  await inlineImages(clone, options);
@@ -2512,28 +2818,28 @@ async function captureDOM(element, options) {
2512
2818
  }
2513
2819
  await new Promise((resolve) => {
2514
2820
  idle(() => {
2515
- const rect = getNaturalBorderBoxSize(element);
2516
- let w = rect.width;
2517
- let h = rect.height;
2821
+ const rect = element.getBoundingClientRect();
2822
+ let w = Number(rect.width) || 0;
2823
+ let h = Number(rect.height) || 0;
2518
2824
  const hasW = Number.isFinite(options.width);
2519
2825
  const hasH = Number.isFinite(options.height);
2520
2826
  const scaleOpt = options.scale ?? 1;
2521
2827
  const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2522
2828
  if (!hasScale) {
2523
- const aspect = rect.width / rect.height;
2829
+ const aspect = h > 0 ? w / h : 1;
2524
2830
  if (hasW && hasH) {
2525
- w = options.width;
2526
- h = options.height;
2831
+ w = Number(options.width);
2832
+ h = Number(options.height);
2527
2833
  } else if (hasW) {
2528
- w = options.width;
2529
- h = w / aspect;
2834
+ w = Number(options.width);
2835
+ h = w / (aspect || 1);
2530
2836
  } else if (hasH) {
2531
- h = options.height;
2532
- w = h * aspect;
2837
+ h = Number(options.height);
2838
+ w = h * (aspect || 1);
2533
2839
  }
2534
2840
  }
2535
- w = Math.max(1, Math.ceil(w));
2536
- h = Math.max(1, Math.ceil(h));
2841
+ w = Math.max(1, Math.ceil(Number.isFinite(w) ? w : 1));
2842
+ h = Math.max(1, Math.ceil(Number.isFinite(h) ? h : 1));
2537
2843
  const csEl = getComputedStyle(element);
2538
2844
  const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2539
2845
  const ind = readIndividualTransforms(element);
@@ -2604,7 +2910,9 @@ async function captureDOM(element, options) {
2604
2910
  fo.appendChild(container);
2605
2911
  const serializer = new XMLSerializer();
2606
2912
  const foString = serializer.serializeToString(fo);
2607
- const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
2913
+ const finalW = !hasScale && (hasW || hasH) ? w : outW;
2914
+ const finalH = !hasScale && (hasH || hasW) ? h : outH;
2915
+ const svgHeader = `<svg xmlns="${svgNS}" width="${finalW}" height="${finalH}" viewBox="0 0 ${outW} ${outH}">`;
2608
2916
  const svgFooter = "</svg>";
2609
2917
  svgString = svgHeader + foString + svgFooter;
2610
2918
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2615,17 +2923,6 @@ async function captureDOM(element, options) {
2615
2923
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2616
2924
  return dataURL;
2617
2925
  }
2618
- function getNaturalBorderBoxSize(el) {
2619
- const cs = getComputedStyle(el);
2620
- const px = (v) => parseFloat(v) || 0;
2621
- let w = px(cs.width);
2622
- let h = px(cs.height);
2623
- if (cs.boxSizing === "content-box") {
2624
- w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
2625
- h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
2626
- }
2627
- return { width: w, height: h };
2628
- }
2629
2926
  function matrixFromComputed(el) {
2630
2927
  const tr = getComputedStyle(el).transform;
2631
2928
  if (!tr || tr === "none") return new DOMMatrix();
@@ -2657,24 +2954,33 @@ function bboxFromMatrix(w, h, M2D) {
2657
2954
  }
2658
2955
  function readIndividualTransforms(el) {
2659
2956
  const out = { rotate: "0deg", scale: null, translate: null };
2660
- const map = el.computedStyleMap?.();
2957
+ const map = typeof el.computedStyleMap === "function" ? el.computedStyleMap() : null;
2661
2958
  if (map) {
2662
- const rot = map.get("rotate");
2959
+ const safeGet = (prop) => {
2960
+ try {
2961
+ if (typeof map.has === "function" && !map.has(prop)) return null;
2962
+ if (typeof map.get !== "function") return null;
2963
+ return map.get(prop);
2964
+ } catch {
2965
+ return null;
2966
+ }
2967
+ };
2968
+ const rot = safeGet("rotate");
2663
2969
  if (rot) {
2664
2970
  const ang = rot.angle ?? rot;
2665
- if (ang && "unit" in ang) {
2971
+ if (ang && typeof ang === "object" && "unit" in ang) {
2666
2972
  out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2667
2973
  } else {
2668
2974
  out.rotate = String(rot);
2669
2975
  }
2670
2976
  }
2671
- const sc = map.get("scale");
2977
+ const sc = safeGet("scale");
2672
2978
  if (sc && sc.length) {
2673
2979
  const sx = sc[0]?.value ?? 1;
2674
2980
  const sy = sc[1]?.value ?? sx;
2675
2981
  out.scale = `${sx} ${sy}`;
2676
2982
  }
2677
- const tr = map.get("translate");
2983
+ const tr = safeGet("translate");
2678
2984
  if (tr && tr.length) {
2679
2985
  const tx = tr[0]?.value ?? 0;
2680
2986
  const ty = tr[1]?.value ?? 0;
@@ -2759,9 +3065,9 @@ function createContext(options = {}) {
2759
3065
  scale: options.scale ?? 1,
2760
3066
  // DOM filters
2761
3067
  exclude: options.exclude ?? [],
2762
- excludeMode: options.excludeMode ?? "visuallyHide",
3068
+ excludeMode: options.excludeMode ?? "hide",
2763
3069
  filter: options.filter ?? null,
2764
- filterMode: options.filterMode ?? "visuallyHide",
3070
+ filterMode: options.filterMode ?? "hide",
2765
3071
  placeholders: options.placeholders !== false,
2766
3072
  // default true
2767
3073
  // Fuentes
@@ -2769,7 +3075,7 @@ function createContext(options = {}) {
2769
3075
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2770
3076
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2771
3077
  excludeFonts: options.excludeFonts ?? void 0,
2772
- defaultImageUrl: options.defaultImageUrl ?? void 0,
3078
+ fallbackURL: options.fallbackURL ?? void 0,
2773
3079
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2774
3080
  /** @type {CachePolicy} */
2775
3081
  cache: cachePolicy,
@@ -2855,6 +3161,7 @@ async function rasterize(url, options) {
2855
3161
 
2856
3162
  // src/exporters/download.js
2857
3163
  async function download(url, options) {
3164
+ options.dpr = 1;
2858
3165
  if (options.format === "svg") {
2859
3166
  const blob = await toBlob(url, { ...options, type: "svg" });
2860
3167
  const objectURL = URL.createObjectURL(blob);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.9.12-dev.0",
3
+ "version": "1.9.12-dev.2",
4
4
  "description": "snapDOM captures HTML elements to images with exceptional speed and accuracy.",
5
5
  "main": "dist/snapdom.js",
6
6
  "module": "dist/snapdom.mjs",
@@ -13,8 +13,8 @@ export type BlobType = "svg" | RasterMime;
13
13
  export type IconFontMatcher = string | RegExp;
14
14
  export type CachePolicy = "disabled" | "full" | "auto" | "soft";
15
15
 
16
- export type ExcludeMode = 'visuallyHide' | 'remove';
17
- export type FilterMode = 'visuallyHide' | 'remove';
16
+ export type ExcludeMode = 'hide' | 'remove';
17
+ export type FilterMode = 'hide' | 'remove';
18
18
 
19
19
  export interface LocalFontDescriptor {
20
20
  /** CSS font-family name (e.g. "Inter"). */
@@ -92,7 +92,7 @@ export interface CaptureOptions {
92
92
 
93
93
  /**
94
94
  * Mode applied to excluded nodes of the cloned tree.
95
- * Default: "visuallyHide"
95
+ * Default: "hide"
96
96
  */
97
97
  excludeMode?: ExcludeMode;
98
98
 
@@ -104,7 +104,7 @@ export interface CaptureOptions {
104
104
 
105
105
  /**
106
106
  * Mode applied to filtered nodes of the cloned tree.
107
- * Default: "visuallyHide"
107
+ * Default: "hide"
108
108
  */
109
109
  filterMode?: FilterMode;
110
110
 
@@ -141,7 +141,7 @@ export interface CaptureOptions {
141
141
  * - String: use as-is.
142
142
  * - Callback: receives measured width/height and original src, returns a URL string.
143
143
  */
144
- defaultImageUrl?:
144
+ fallbackURL?:
145
145
  | string
146
146
  | ((
147
147
  args: {