@zumer/snapdom 1.9.12-dev.0 → 1.9.12-dev.1

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.1
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,11 @@ 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);
691
712
  snapshotCache.set(el, { epoch: __epoch, snapshot: snap });
692
713
  return snap;
693
714
  }
@@ -741,7 +762,7 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
741
762
  session.styleCache.set(source, getComputedStyle(source));
742
763
  }
743
764
  const pre = session.styleCache.get(source);
744
- const snap = getSnapshot(source, pre);
765
+ const snap = getSnapshot(source, pre, ctx.options);
745
766
  const sig = styleSignature(snap);
746
767
  let key = persist.snapshotKeyCache.get(sig);
747
768
  if (!key) {
@@ -985,7 +1006,7 @@ async function deepClone(node, sessionCache, options) {
985
1006
  return node.cloneNode(true);
986
1007
  }
987
1008
  if (node.getAttribute("data-capture") === "exclude") {
988
- if (options.excludeMode === "visuallyHide") {
1009
+ if (options.excludeMode === "hide") {
989
1010
  const spacer = document.createElement("div");
990
1011
  const rect = node.getBoundingClientRect();
991
1012
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -998,7 +1019,7 @@ async function deepClone(node, sessionCache, options) {
998
1019
  for (const selector of options.exclude) {
999
1020
  try {
1000
1021
  if (node.matches?.(selector)) {
1001
- if (options.excludeMode === "visuallyHide") {
1022
+ if (options.excludeMode === "hide") {
1002
1023
  const spacer = document.createElement("div");
1003
1024
  const rect = node.getBoundingClientRect();
1004
1025
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -1015,7 +1036,7 @@ async function deepClone(node, sessionCache, options) {
1015
1036
  if (typeof options.filter === "function") {
1016
1037
  try {
1017
1038
  if (!options.filter(node)) {
1018
- if (options.filterMode === "visuallyHide") {
1039
+ if (options.filterMode === "hide") {
1019
1040
  const spacer = document.createElement("div");
1020
1041
  const rect = node.getBoundingClientRect();
1021
1042
  spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
@@ -1898,7 +1919,182 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
1898
1919
  }
1899
1920
  }
1900
1921
 
1922
+ // src/modules/counter.js
1923
+ function hasCounters(input) {
1924
+ return /\bcounter\s*\(|\bcounters\s*\(/.test(input || "");
1925
+ }
1926
+ function unquoteDoubleStrings(s) {
1927
+ return (s || "").replace(/"([^"]*)"/g, "$1");
1928
+ }
1929
+ function alpha(n, upper = false) {
1930
+ let s = "", x = Math.max(1, n);
1931
+ while (x > 0) {
1932
+ x--;
1933
+ s = String.fromCharCode(97 + x % 26) + s;
1934
+ x = Math.floor(x / 26);
1935
+ }
1936
+ return upper ? s.toUpperCase() : s;
1937
+ }
1938
+ function roman(n, upper = true) {
1939
+ 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"]];
1940
+ let num = Math.max(1, Math.min(3999, n)), out = "";
1941
+ for (const [v, sym] of map) while (num >= v) {
1942
+ out += sym;
1943
+ num -= v;
1944
+ }
1945
+ return upper ? out : out.toLowerCase();
1946
+ }
1947
+ function formatCounter(value, style) {
1948
+ switch ((style || "decimal").toLowerCase()) {
1949
+ case "decimal":
1950
+ return String(Math.max(0, value));
1951
+ case "decimal-leading-zero":
1952
+ return (value < 10 ? "0" : "") + String(Math.max(0, value));
1953
+ case "lower-alpha":
1954
+ return alpha(value, false);
1955
+ case "upper-alpha":
1956
+ return alpha(value, true);
1957
+ case "lower-roman":
1958
+ return roman(value, false);
1959
+ case "upper-roman":
1960
+ return roman(value, true);
1961
+ default:
1962
+ return String(Math.max(0, value));
1963
+ }
1964
+ }
1965
+ function buildCounterContext(root) {
1966
+ const nodeCounters = /* @__PURE__ */ new WeakMap();
1967
+ const rootEl = root instanceof Document ? root.documentElement : root;
1968
+ const isLi = (el) => el && el.tagName === "LI";
1969
+ const countPrevLi = (li) => {
1970
+ let c = 0, p = li?.parentElement;
1971
+ if (!p) return 0;
1972
+ for (const sib of p.children) {
1973
+ if (sib === li) break;
1974
+ if (sib.tagName === "LI") c++;
1975
+ }
1976
+ return c;
1977
+ };
1978
+ const cloneMap = (m) => {
1979
+ const out = /* @__PURE__ */ new Map();
1980
+ for (const [k, arr] of m) out.set(k, arr.slice());
1981
+ return out;
1982
+ };
1983
+ const applyTo = (baseMap, parentMap, el) => {
1984
+ const map = cloneMap(baseMap);
1985
+ let reset;
1986
+ try {
1987
+ reset = el.style?.counterReset || getComputedStyle(el).counterReset;
1988
+ } catch {
1989
+ }
1990
+ if (reset && reset !== "none") {
1991
+ for (const part of reset.split(",")) {
1992
+ const toks = part.trim().split(/\s+/);
1993
+ const name = toks[0];
1994
+ const val = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 0;
1995
+ if (!name) continue;
1996
+ const parentStack = parentMap.get(name);
1997
+ if (parentStack && parentStack.length) {
1998
+ const s = parentStack.slice();
1999
+ s.push(val);
2000
+ map.set(name, s);
2001
+ } else {
2002
+ map.set(name, [val]);
2003
+ }
2004
+ }
2005
+ }
2006
+ let inc;
2007
+ try {
2008
+ inc = el.style?.counterIncrement || getComputedStyle(el).counterIncrement;
2009
+ } catch {
2010
+ }
2011
+ if (inc && inc !== "none") {
2012
+ for (const part of inc.split(",")) {
2013
+ const toks = part.trim().split(/\s+/);
2014
+ const name = toks[0];
2015
+ const by = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 1;
2016
+ if (!name) continue;
2017
+ const stack = map.get(name) || [];
2018
+ if (stack.length === 0) stack.push(0);
2019
+ stack[stack.length - 1] += by;
2020
+ map.set(name, stack);
2021
+ }
2022
+ }
2023
+ try {
2024
+ const cs = getComputedStyle(el);
2025
+ if (cs.display === "list-item" && isLi(el)) {
2026
+ const p = el.parentElement;
2027
+ let idx = 1;
2028
+ if (p && p.tagName === "OL") {
2029
+ const startAttr = p.getAttribute("start");
2030
+ const start = Number.isFinite(Number(startAttr)) ? Number(startAttr) : 1;
2031
+ const prev = countPrevLi(el);
2032
+ const ownAttr = el.getAttribute("value");
2033
+ idx = Number.isFinite(Number(ownAttr)) ? Number(ownAttr) : start + prev;
2034
+ } else {
2035
+ idx = 1 + countPrevLi(el);
2036
+ }
2037
+ const s = map.get("list-item") || [];
2038
+ if (s.length === 0) s.push(0);
2039
+ s[s.length - 1] = idx;
2040
+ map.set("list-item", s);
2041
+ }
2042
+ } catch {
2043
+ }
2044
+ return map;
2045
+ };
2046
+ const build = (el, parentMap, carryMap) => {
2047
+ const curr = applyTo(carryMap, parentMap, el);
2048
+ nodeCounters.set(el, curr);
2049
+ let nextCarry = curr;
2050
+ for (const child of el.children) {
2051
+ const childCarry = build(child, curr, nextCarry);
2052
+ nextCarry = childCarry;
2053
+ }
2054
+ return curr;
2055
+ };
2056
+ const empty = /* @__PURE__ */ new Map();
2057
+ build(rootEl, empty, empty);
2058
+ return {
2059
+ get(node, name) {
2060
+ const s = nodeCounters.get(node)?.get(name);
2061
+ return s && s.length ? s[s.length - 1] : 0;
2062
+ },
2063
+ getStack(node, name) {
2064
+ const s = nodeCounters.get(node)?.get(name);
2065
+ return s ? s.slice() : [];
2066
+ }
2067
+ };
2068
+ }
2069
+ function resolveCountersInContent(raw, node, ctx) {
2070
+ if (!raw || raw === "none") return raw;
2071
+ try {
2072
+ const RX = /\b(counter|counters)\s*\(([^)]+)\)/g;
2073
+ let out = raw.replace(RX, (_, fn, args) => {
2074
+ const parts = String(args).split(",").map((s) => s.trim());
2075
+ if (fn === "counter") {
2076
+ const name = parts[0]?.replace(/^["']|["']$/g, "");
2077
+ const style = (parts[1] || "decimal").toLowerCase();
2078
+ const v = ctx.get(node, name);
2079
+ return formatCounter(v, style);
2080
+ } else {
2081
+ const name = parts[0]?.replace(/^["']|["']$/g, "");
2082
+ const sep = parts[1]?.replace(/^["']|["']$/g, "") ?? "";
2083
+ const style = (parts[2] || "decimal").toLowerCase();
2084
+ const stack = ctx.getStack(node, name);
2085
+ if (!stack.length) return "";
2086
+ const pieces = stack.map((v) => formatCounter(v, style));
2087
+ return pieces.join(sep);
2088
+ }
2089
+ });
2090
+ return unquoteDoubleStrings(out);
2091
+ } catch {
2092
+ return "- ";
2093
+ }
2094
+ }
2095
+
1901
2096
  // src/modules/pseudo.js
2097
+ var counterCtx = null;
1902
2098
  async function inlinePseudoElements(source, clone, sessionCache, options) {
1903
2099
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
1904
2100
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
@@ -1932,7 +2128,11 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
1932
2128
  continue;
1933
2129
  }
1934
2130
  const content = style.content;
1935
- const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
2131
+ let cleanContent = parseContent(content);
2132
+ if (hasCounters(content)) {
2133
+ counterCtx = buildCounterContext(source.ownerDocument || document);
2134
+ cleanContent = resolveCountersInContent(cleanContent, source, counterCtx);
2135
+ }
1936
2136
  const bg = style.backgroundImage;
1937
2137
  const bgColor = style.backgroundColor;
1938
2138
  const fontFamily = style.fontFamily;
@@ -2317,8 +2517,8 @@ async function inlineImages(clone, options = {}) {
2317
2517
  if (!img.height) img.height = img.naturalHeight || 100;
2318
2518
  return;
2319
2519
  }
2320
- const { defaultImageUrl } = options || {};
2321
- if (defaultImageUrl) {
2520
+ const { fallbackURL } = options || {};
2521
+ if (fallbackURL) {
2322
2522
  try {
2323
2523
  const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
2324
2524
  const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
@@ -2326,14 +2526,14 @@ async function inlineImages(clone, options = {}) {
2326
2526
  const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
2327
2527
  const styleW = parseFloat(img.style?.width || "") || 0;
2328
2528
  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;
2529
+ const fbW = dsW || styleW || attrW || img.width || void 0;
2530
+ const fbH = dsH || styleH || attrH || img.height || void 0;
2531
+ const fallbackUrl = typeof fallbackURL === "function" ? await fallbackURL({ width: fbW, height: fbH, src, element: img }) : fallbackURL;
2332
2532
  if (fallbackUrl) {
2333
2533
  const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
2334
2534
  img.src = fallbackData.data;
2335
- if (!img.width && width) img.width = width;
2336
- if (!img.height && height) img.height = height;
2535
+ if (!img.width && fbW) img.width = fbW;
2536
+ if (!img.height && fbH) img.height = fbH;
2337
2537
  if (!img.width) img.width = img.naturalWidth || 100;
2338
2538
  if (!img.height) img.height = img.naturalHeight || 100;
2339
2539
  return;
@@ -2452,6 +2652,63 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2452
2652
  }
2453
2653
  }
2454
2654
 
2655
+ // src/modules/lineClamp.js
2656
+ function lineClamp(el) {
2657
+ if (!el) return () => {
2658
+ };
2659
+ const lines = getClamp(el);
2660
+ if (lines <= 0) return () => {
2661
+ };
2662
+ if (!isPlainTextContainer(el)) return () => {
2663
+ };
2664
+ const cs = getComputedStyle(el);
2665
+ const targetH = Math.round(usedLineHeightPx(cs) * lines + vpad(cs));
2666
+ const original = el.textContent ?? "";
2667
+ const prevText = original;
2668
+ if (el.scrollHeight <= targetH + 0.5) {
2669
+ return () => {
2670
+ };
2671
+ }
2672
+ let lo = 0, hi = original.length, best = -1;
2673
+ while (lo <= hi) {
2674
+ const mid = lo + hi >> 1;
2675
+ el.textContent = original.slice(0, mid) + "\u2026";
2676
+ if (el.scrollHeight <= targetH + 0.5) {
2677
+ best = mid;
2678
+ lo = mid + 1;
2679
+ } else {
2680
+ hi = mid - 1;
2681
+ }
2682
+ }
2683
+ el.textContent = (best >= 0 ? original.slice(0, best) : "") + "\u2026";
2684
+ return () => {
2685
+ el.textContent = prevText;
2686
+ };
2687
+ }
2688
+ function getClamp(el) {
2689
+ const cs = getComputedStyle(el);
2690
+ let v = cs.getPropertyValue("-webkit-line-clamp") || cs.getPropertyValue("line-clamp");
2691
+ v = (v || "").trim();
2692
+ const n = parseInt(v, 10);
2693
+ return Number.isFinite(n) && n > 0 ? n : 0;
2694
+ }
2695
+ function usedLineHeightPx(cs) {
2696
+ const lh = (cs.lineHeight || "").trim();
2697
+ const fs = parseFloat(cs.fontSize) || 16;
2698
+ if (!lh || lh === "normal") return Math.round(fs * 1.2);
2699
+ if (lh.endsWith("px")) return parseFloat(lh);
2700
+ if (/^\d+(\.\d+)?$/.test(lh)) return Math.round(parseFloat(lh) * fs);
2701
+ if (lh.endsWith("%")) return Math.round(parseFloat(lh) / 100 * fs);
2702
+ return Math.round(fs * 1.2);
2703
+ }
2704
+ function vpad(cs) {
2705
+ return (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
2706
+ }
2707
+ function isPlainTextContainer(el) {
2708
+ if (el.childElementCount > 0) return false;
2709
+ return Array.from(el.childNodes).some((n) => n.nodeType === Node.TEXT_NODE);
2710
+ }
2711
+
2455
2712
  // src/core/capture.js
2456
2713
  async function captureDOM(element, options) {
2457
2714
  if (!element) throw new Error("Element cannot be null or undefined");
@@ -2462,7 +2719,12 @@ async function captureDOM(element, options) {
2462
2719
  let baseCSS = "";
2463
2720
  let dataURL;
2464
2721
  let svgString;
2465
- ({ clone, classCSS, styleCache } = await prepareClone(element, options));
2722
+ const undoClamp = lineClamp(element);
2723
+ try {
2724
+ ({ clone, classCSS, styleCache } = await prepareClone(element, options));
2725
+ } finally {
2726
+ undoClamp();
2727
+ }
2466
2728
  await new Promise((resolve) => {
2467
2729
  idle(async () => {
2468
2730
  await inlineImages(clone, options);
@@ -2512,28 +2774,28 @@ async function captureDOM(element, options) {
2512
2774
  }
2513
2775
  await new Promise((resolve) => {
2514
2776
  idle(() => {
2515
- const rect = getNaturalBorderBoxSize(element);
2516
- let w = rect.width;
2517
- let h = rect.height;
2777
+ const rect = element.getBoundingClientRect();
2778
+ let w = Number(rect.width) || 0;
2779
+ let h = Number(rect.height) || 0;
2518
2780
  const hasW = Number.isFinite(options.width);
2519
2781
  const hasH = Number.isFinite(options.height);
2520
2782
  const scaleOpt = options.scale ?? 1;
2521
2783
  const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2522
2784
  if (!hasScale) {
2523
- const aspect = rect.width / rect.height;
2785
+ const aspect = h > 0 ? w / h : 1;
2524
2786
  if (hasW && hasH) {
2525
- w = options.width;
2526
- h = options.height;
2787
+ w = Number(options.width);
2788
+ h = Number(options.height);
2527
2789
  } else if (hasW) {
2528
- w = options.width;
2529
- h = w / aspect;
2790
+ w = Number(options.width);
2791
+ h = w / (aspect || 1);
2530
2792
  } else if (hasH) {
2531
- h = options.height;
2532
- w = h * aspect;
2793
+ h = Number(options.height);
2794
+ w = h * (aspect || 1);
2533
2795
  }
2534
2796
  }
2535
- w = Math.max(1, Math.ceil(w));
2536
- h = Math.max(1, Math.ceil(h));
2797
+ w = Math.max(1, Math.ceil(Number.isFinite(w) ? w : 1));
2798
+ h = Math.max(1, Math.ceil(Number.isFinite(h) ? h : 1));
2537
2799
  const csEl = getComputedStyle(element);
2538
2800
  const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2539
2801
  const ind = readIndividualTransforms(element);
@@ -2604,7 +2866,9 @@ async function captureDOM(element, options) {
2604
2866
  fo.appendChild(container);
2605
2867
  const serializer = new XMLSerializer();
2606
2868
  const foString = serializer.serializeToString(fo);
2607
- const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
2869
+ const finalW = !hasScale && (hasW || hasH) ? w : outW;
2870
+ const finalH = !hasScale && (hasH || hasW) ? h : outH;
2871
+ const svgHeader = `<svg xmlns="${svgNS}" width="${finalW}" height="${finalH}" viewBox="0 0 ${outW} ${outH}">`;
2608
2872
  const svgFooter = "</svg>";
2609
2873
  svgString = svgHeader + foString + svgFooter;
2610
2874
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2615,17 +2879,6 @@ async function captureDOM(element, options) {
2615
2879
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2616
2880
  return dataURL;
2617
2881
  }
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
2882
  function matrixFromComputed(el) {
2630
2883
  const tr = getComputedStyle(el).transform;
2631
2884
  if (!tr || tr === "none") return new DOMMatrix();
@@ -2657,24 +2910,33 @@ function bboxFromMatrix(w, h, M2D) {
2657
2910
  }
2658
2911
  function readIndividualTransforms(el) {
2659
2912
  const out = { rotate: "0deg", scale: null, translate: null };
2660
- const map = el.computedStyleMap?.();
2913
+ const map = typeof el.computedStyleMap === "function" ? el.computedStyleMap() : null;
2661
2914
  if (map) {
2662
- const rot = map.get("rotate");
2915
+ const safeGet = (prop) => {
2916
+ try {
2917
+ if (typeof map.has === "function" && !map.has(prop)) return null;
2918
+ if (typeof map.get !== "function") return null;
2919
+ return map.get(prop);
2920
+ } catch {
2921
+ return null;
2922
+ }
2923
+ };
2924
+ const rot = safeGet("rotate");
2663
2925
  if (rot) {
2664
2926
  const ang = rot.angle ?? rot;
2665
- if (ang && "unit" in ang) {
2927
+ if (ang && typeof ang === "object" && "unit" in ang) {
2666
2928
  out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2667
2929
  } else {
2668
2930
  out.rotate = String(rot);
2669
2931
  }
2670
2932
  }
2671
- const sc = map.get("scale");
2933
+ const sc = safeGet("scale");
2672
2934
  if (sc && sc.length) {
2673
2935
  const sx = sc[0]?.value ?? 1;
2674
2936
  const sy = sc[1]?.value ?? sx;
2675
2937
  out.scale = `${sx} ${sy}`;
2676
2938
  }
2677
- const tr = map.get("translate");
2939
+ const tr = safeGet("translate");
2678
2940
  if (tr && tr.length) {
2679
2941
  const tx = tr[0]?.value ?? 0;
2680
2942
  const ty = tr[1]?.value ?? 0;
@@ -2759,9 +3021,9 @@ function createContext(options = {}) {
2759
3021
  scale: options.scale ?? 1,
2760
3022
  // DOM filters
2761
3023
  exclude: options.exclude ?? [],
2762
- excludeMode: options.excludeMode ?? "visuallyHide",
3024
+ excludeMode: options.excludeMode ?? "hide",
2763
3025
  filter: options.filter ?? null,
2764
- filterMode: options.filterMode ?? "visuallyHide",
3026
+ filterMode: options.filterMode ?? "hide",
2765
3027
  placeholders: options.placeholders !== false,
2766
3028
  // default true
2767
3029
  // Fuentes
@@ -2769,7 +3031,7 @@ function createContext(options = {}) {
2769
3031
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2770
3032
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2771
3033
  excludeFonts: options.excludeFonts ?? void 0,
2772
- defaultImageUrl: options.defaultImageUrl ?? void 0,
3034
+ fallbackURL: options.fallbackURL ?? void 0,
2773
3035
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2774
3036
  /** @type {CachePolicy} */
2775
3037
  cache: cachePolicy,
@@ -2855,6 +3117,7 @@ async function rasterize(url, options) {
2855
3117
 
2856
3118
  // src/exporters/download.js
2857
3119
  async function download(url, options) {
3120
+ options.dpr = 1;
2858
3121
  if (options.format === "svg") {
2859
3122
  const blob = await toBlob(url, { ...options, type: "svg" });
2860
3123
  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.1",
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: {