@zumer/snapdom 1.9.11 → 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.10
4
+ * v.1.9.12-dev.1
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -127,10 +127,25 @@ var snapLogger = createSnapLogger("[snapDOM]", { ttlMs: 3 * 6e4, maxEntries: 10
127
127
  var _inflight = /* @__PURE__ */ new Map();
128
128
  var _errorCache = /* @__PURE__ */ new Map();
129
129
  function isSpecialURL(url) {
130
- return /^data:|^blob:|^about:blank$/.test(url);
130
+ return /^data:|^blob:|^about:blank$/i.test(url);
131
+ }
132
+ function isAlreadyProxied(url, useProxy) {
133
+ try {
134
+ const baseHref = typeof location !== "undefined" && location.href ? location.href : "http://localhost/";
135
+ const proxyBaseRaw = useProxy.includes("{url}") ? useProxy.split("{url}")[0] : useProxy;
136
+ const proxyBase = new URL(proxyBaseRaw || ".", baseHref);
137
+ const u = new URL(url, baseHref);
138
+ if (u.origin === proxyBase.origin) return true;
139
+ const sp = u.searchParams;
140
+ if (sp && (sp.has("url") || sp.has("target"))) return true;
141
+ } catch {
142
+ }
143
+ return false;
131
144
  }
132
145
  function shouldProxy(url, useProxy) {
133
- if (!useProxy || isSpecialURL(url)) return false;
146
+ if (!useProxy) return false;
147
+ if (isSpecialURL(url)) return false;
148
+ if (isAlreadyProxied(url, useProxy)) return false;
134
149
  try {
135
150
  const base = typeof location !== "undefined" && location.href ? location.href : "http://localhost/";
136
151
  const u = new URL(url, base);
@@ -141,8 +156,19 @@ function shouldProxy(url, useProxy) {
141
156
  }
142
157
  function applyProxy(url, useProxy) {
143
158
  if (!useProxy) return url;
144
- if (useProxy.includes("{url}")) return useProxy.replace("{url}", encodeURIComponent(url));
145
- const sep = useProxy.endsWith("?") ? "" : useProxy.includes("?") ? "&" : "?";
159
+ if (useProxy.includes("{url}")) {
160
+ return useProxy.replace("{urlRaw}", safeEncodeURI(url)).replace("{url}", encodeURIComponent(url));
161
+ }
162
+ if (/[?&]url=?$/.test(useProxy)) {
163
+ return `${useProxy}${encodeURIComponent(url)}`;
164
+ }
165
+ if (useProxy.endsWith("?")) {
166
+ return `${useProxy}url=${encodeURIComponent(url)}`;
167
+ }
168
+ if (useProxy.endsWith("/")) {
169
+ return `${useProxy}${safeEncodeURI(url)}`;
170
+ }
171
+ const sep = useProxy.includes("?") ? "&" : "?";
146
172
  return `${useProxy}${sep}url=${encodeURIComponent(url)}`;
147
173
  }
148
174
  function blobToDataURL(blob) {
@@ -169,6 +195,65 @@ async function snapFetch(url, options = {}) {
169
195
  const errorTTL = options.errorTTL ?? 8e3;
170
196
  const headers = options.headers || {};
171
197
  const silent = !!options.silent;
198
+ if (/^data:/i.test(url)) {
199
+ try {
200
+ if (as === "text") {
201
+ return { ok: true, data: String(url), status: 200, url, fromCache: false };
202
+ }
203
+ if (as === "dataURL") {
204
+ return {
205
+ ok: true,
206
+ data: String(url),
207
+ status: 200,
208
+ url,
209
+ fromCache: false,
210
+ mime: String(url).slice(5).split(";")[0] || ""
211
+ };
212
+ }
213
+ const [, meta = "", data = ""] = String(url).match(/^data:([^,]*),(.*)$/) || [];
214
+ const isBase64 = /;base64/i.test(meta);
215
+ const bin = isBase64 ? atob(data) : decodeURIComponent(data);
216
+ const bytes = new Uint8Array([...bin].map((c) => c.charCodeAt(0)));
217
+ const b = new Blob([bytes], { type: (meta || "").split(";")[0] || "" });
218
+ return { ok: true, data: b, status: 200, url, fromCache: false, mime: b.type || "" };
219
+ } catch {
220
+ return { ok: false, data: null, status: 0, url, fromCache: false, reason: "special_url_error" };
221
+ }
222
+ }
223
+ if (/^blob:/i.test(url)) {
224
+ try {
225
+ const resp = await fetch(url);
226
+ if (!resp.ok) {
227
+ return { ok: false, data: null, status: resp.status, url, fromCache: false, reason: "http_error" };
228
+ }
229
+ const blob = await resp.blob();
230
+ const mime = blob.type || resp.headers.get("content-type") || "";
231
+ if (as === "dataURL") {
232
+ const dataURL = await blobToDataURL(blob);
233
+ return { ok: true, data: dataURL, status: resp.status, url, fromCache: false, mime };
234
+ }
235
+ if (as === "text") {
236
+ const text = await blob.text();
237
+ return { ok: true, data: text, status: resp.status, url, fromCache: false, mime };
238
+ }
239
+ return { ok: true, data: blob, status: resp.status, url, fromCache: false, mime };
240
+ } catch {
241
+ return { ok: false, data: null, status: 0, url, fromCache: false, reason: "network" };
242
+ }
243
+ }
244
+ if (/^about:blank$/i.test(url)) {
245
+ if (as === "dataURL") {
246
+ return {
247
+ ok: true,
248
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==",
249
+ status: 200,
250
+ url,
251
+ fromCache: false,
252
+ mime: "image/png"
253
+ };
254
+ }
255
+ return { ok: true, data: as === "text" ? "" : new Blob([]), status: 200, url, fromCache: false };
256
+ }
172
257
  const key = makeKey(url, { as, timeout, useProxy, errorTTL });
173
258
  const e = _errorCache.get(key);
174
259
  if (e && e.until > Date.now()) {
@@ -197,7 +282,7 @@ async function snapFetch(url, options = {}) {
197
282
  const resp = await fetch(finalURL, { signal: ctrl.signal, credentials: cred, headers });
198
283
  if (!resp.ok) {
199
284
  const result = { ok: false, data: null, status: resp.status, url: finalURL, fromCache: false, reason: "http_error" };
200
- _errorCache.set(key, { until: Date.now() + errorTTL, result });
285
+ if (errorTTL > 0) _errorCache.set(key, { until: Date.now() + errorTTL, result });
201
286
  if (!silent) {
202
287
  const short = `${resp.status} ${resp.statusText || ""}`.trim();
203
288
  snapLogger.warnOnce(
@@ -222,7 +307,9 @@ async function snapFetch(url, options = {}) {
222
307
  } catch (err) {
223
308
  const reason = err && typeof err === "object" && "name" in err && err.name === "AbortError" ? String(err.message || "").includes("timeout") ? "timeout" : "abort" : "network";
224
309
  const result = { ok: false, data: null, status: 0, url: finalURL, fromCache: false, reason };
225
- _errorCache.set(key, { until: Date.now() + errorTTL, result });
310
+ if (!/^blob:/i.test(url) && errorTTL > 0) {
311
+ _errorCache.set(key, { until: Date.now() + errorTTL, result });
312
+ }
226
313
  if (!silent) {
227
314
  const k = `${reason}:${as}:${new URL(url, location?.href ?? "http://localhost/").origin}`;
228
315
  const tips = reason === "timeout" ? `Timeout after ${timeout}ms. Consider increasing timeout or using a proxy for ${url}` : reason === "abort" ? `Request aborted while fetching ${as} ${url}` : `Network/CORS issue while fetching ${as} ${url}. A proxy may be required`;
@@ -539,7 +626,10 @@ function idle(fn, { fast = false } = {}) {
539
626
  }
540
627
  }
541
628
  function isSafari() {
542
- 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;
543
633
  }
544
634
 
545
635
  // src/modules/styles.js
@@ -573,7 +663,7 @@ function setupInvalidationOnce(root = document.documentElement) {
573
663
  } catch {
574
664
  }
575
665
  }
576
- function snapshotComputedStyleFull(style) {
666
+ function snapshotComputedStyleFull(style, options = {}) {
577
667
  const out = {};
578
668
  const vis = style.getPropertyValue("visibility");
579
669
  for (let i = 0; i < style.length; i++) {
@@ -584,6 +674,24 @@ function snapshotComputedStyleFull(style) {
584
674
  }
585
675
  out[prop] = val;
586
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
+ }
587
695
  if (vis === "hidden") out.opacity = "0";
588
696
  return out;
589
697
  }
@@ -596,11 +704,11 @@ function styleSignature(snap) {
596
704
  __snapshotSig.set(snap, sig);
597
705
  return sig;
598
706
  }
599
- function getSnapshot(el, preStyle = null) {
707
+ function getSnapshot(el, preStyle = null, options = {}) {
600
708
  const rec = snapshotCache.get(el);
601
709
  if (rec && rec.epoch === __epoch) return rec.snapshot;
602
710
  const style = preStyle || getComputedStyle(el);
603
- const snap = snapshotComputedStyleFull(style);
711
+ const snap = snapshotComputedStyleFull(style, options);
604
712
  snapshotCache.set(el, { epoch: __epoch, snapshot: snap });
605
713
  return snap;
606
714
  }
@@ -654,7 +762,7 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
654
762
  session.styleCache.set(source, getComputedStyle(source));
655
763
  }
656
764
  const pre = session.styleCache.get(source);
657
- const snap = getSnapshot(source, pre);
765
+ const snap = getSnapshot(source, pre, ctx.options);
658
766
  const sig = styleSignature(snap);
659
767
  let key = persist.snapshotKeyCache.get(sig);
660
768
  if (!key) {
@@ -898,19 +1006,27 @@ async function deepClone(node, sessionCache, options) {
898
1006
  return node.cloneNode(true);
899
1007
  }
900
1008
  if (node.getAttribute("data-capture") === "exclude") {
901
- const spacer = document.createElement("div");
902
- const rect = node.getBoundingClientRect();
903
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
904
- return spacer;
1009
+ if (options.excludeMode === "hide") {
1010
+ const spacer = document.createElement("div");
1011
+ const rect = node.getBoundingClientRect();
1012
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
1013
+ return spacer;
1014
+ } else if (options.excludeMode === "remove") {
1015
+ return null;
1016
+ }
905
1017
  }
906
1018
  if (options.exclude && Array.isArray(options.exclude)) {
907
1019
  for (const selector of options.exclude) {
908
1020
  try {
909
1021
  if (node.matches?.(selector)) {
910
- const spacer = document.createElement("div");
911
- const rect = node.getBoundingClientRect();
912
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
913
- return spacer;
1022
+ if (options.excludeMode === "hide") {
1023
+ const spacer = document.createElement("div");
1024
+ const rect = node.getBoundingClientRect();
1025
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
1026
+ return spacer;
1027
+ } else if (options.excludeMode === "remove") {
1028
+ return null;
1029
+ }
914
1030
  }
915
1031
  } catch (err) {
916
1032
  console.warn(`Invalid selector in exclude option: ${selector}`, err);
@@ -920,10 +1036,14 @@ async function deepClone(node, sessionCache, options) {
920
1036
  if (typeof options.filter === "function") {
921
1037
  try {
922
1038
  if (!options.filter(node)) {
923
- const spacer = document.createElement("div");
924
- const rect = node.getBoundingClientRect();
925
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
926
- return spacer;
1039
+ if (options.filterMode === "hide") {
1040
+ const spacer = document.createElement("div");
1041
+ const rect = node.getBoundingClientRect();
1042
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
1043
+ return spacer;
1044
+ } else if (options.filterMode === "remove") {
1045
+ return null;
1046
+ }
927
1047
  }
928
1048
  } catch (err) {
929
1049
  console.warn("Error in filter function:", err);
@@ -1799,7 +1919,182 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
1799
1919
  }
1800
1920
  }
1801
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
+
1802
2096
  // src/modules/pseudo.js
2097
+ var counterCtx = null;
1803
2098
  async function inlinePseudoElements(source, clone, sessionCache, options) {
1804
2099
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
1805
2100
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
@@ -1833,7 +2128,11 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
1833
2128
  continue;
1834
2129
  }
1835
2130
  const content = style.content;
1836
- 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
+ }
1837
2136
  const bg = style.backgroundImage;
1838
2137
  const bgColor = style.backgroundColor;
1839
2138
  const fontFamily = style.fontFamily;
@@ -2218,8 +2517,8 @@ async function inlineImages(clone, options = {}) {
2218
2517
  if (!img.height) img.height = img.naturalHeight || 100;
2219
2518
  return;
2220
2519
  }
2221
- const { defaultImageUrl } = options || {};
2222
- if (defaultImageUrl) {
2520
+ const { fallbackURL } = options || {};
2521
+ if (fallbackURL) {
2223
2522
  try {
2224
2523
  const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
2225
2524
  const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
@@ -2227,14 +2526,14 @@ async function inlineImages(clone, options = {}) {
2227
2526
  const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
2228
2527
  const styleW = parseFloat(img.style?.width || "") || 0;
2229
2528
  const styleH = parseFloat(img.style?.height || "") || 0;
2230
- const width = dsW || styleW || attrW || img.width || void 0;
2231
- const height = dsH || styleH || attrH || img.height || void 0;
2232
- 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;
2233
2532
  if (fallbackUrl) {
2234
2533
  const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
2235
2534
  img.src = fallbackData.data;
2236
- if (!img.width && width) img.width = width;
2237
- if (!img.height && height) img.height = height;
2535
+ if (!img.width && fbW) img.width = fbW;
2536
+ if (!img.height && fbH) img.height = fbH;
2238
2537
  if (!img.width) img.width = img.naturalWidth || 100;
2239
2538
  if (!img.height) img.height = img.naturalHeight || 100;
2240
2539
  return;
@@ -2353,6 +2652,63 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
2353
2652
  }
2354
2653
  }
2355
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
+
2356
2712
  // src/core/capture.js
2357
2713
  async function captureDOM(element, options) {
2358
2714
  if (!element) throw new Error("Element cannot be null or undefined");
@@ -2363,7 +2719,12 @@ async function captureDOM(element, options) {
2363
2719
  let baseCSS = "";
2364
2720
  let dataURL;
2365
2721
  let svgString;
2366
- ({ 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
+ }
2367
2728
  await new Promise((resolve) => {
2368
2729
  idle(async () => {
2369
2730
  await inlineImages(clone, options);
@@ -2385,7 +2746,7 @@ async function captureDOM(element, options) {
2385
2746
  const families = new Set(
2386
2747
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2387
2748
  );
2388
- await ensureFontsReady(families, 5);
2749
+ await ensureFontsReady(families, 2);
2389
2750
  }
2390
2751
  fontsCSS = await embedCustomFonts({
2391
2752
  required,
@@ -2413,28 +2774,28 @@ async function captureDOM(element, options) {
2413
2774
  }
2414
2775
  await new Promise((resolve) => {
2415
2776
  idle(() => {
2416
- const rect = getNaturalBorderBoxSize(element);
2417
- let w = rect.width;
2418
- let h = rect.height;
2777
+ const rect = element.getBoundingClientRect();
2778
+ let w = Number(rect.width) || 0;
2779
+ let h = Number(rect.height) || 0;
2419
2780
  const hasW = Number.isFinite(options.width);
2420
2781
  const hasH = Number.isFinite(options.height);
2421
2782
  const scaleOpt = options.scale ?? 1;
2422
2783
  const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2423
2784
  if (!hasScale) {
2424
- const aspect = rect.width / rect.height;
2785
+ const aspect = h > 0 ? w / h : 1;
2425
2786
  if (hasW && hasH) {
2426
- w = options.width;
2427
- h = options.height;
2787
+ w = Number(options.width);
2788
+ h = Number(options.height);
2428
2789
  } else if (hasW) {
2429
- w = options.width;
2430
- h = w / aspect;
2790
+ w = Number(options.width);
2791
+ h = w / (aspect || 1);
2431
2792
  } else if (hasH) {
2432
- h = options.height;
2433
- w = h * aspect;
2793
+ h = Number(options.height);
2794
+ w = h * (aspect || 1);
2434
2795
  }
2435
2796
  }
2436
- w = Math.max(1, Math.ceil(w));
2437
- 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));
2438
2799
  const csEl = getComputedStyle(element);
2439
2800
  const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2440
2801
  const ind = readIndividualTransforms(element);
@@ -2505,7 +2866,9 @@ async function captureDOM(element, options) {
2505
2866
  fo.appendChild(container);
2506
2867
  const serializer = new XMLSerializer();
2507
2868
  const foString = serializer.serializeToString(fo);
2508
- 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}">`;
2509
2872
  const svgFooter = "</svg>";
2510
2873
  svgString = svgHeader + foString + svgFooter;
2511
2874
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2516,17 +2879,6 @@ async function captureDOM(element, options) {
2516
2879
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2517
2880
  return dataURL;
2518
2881
  }
2519
- function getNaturalBorderBoxSize(el) {
2520
- const cs = getComputedStyle(el);
2521
- const px = (v) => parseFloat(v) || 0;
2522
- let w = px(cs.width);
2523
- let h = px(cs.height);
2524
- if (cs.boxSizing === "content-box") {
2525
- w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
2526
- h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
2527
- }
2528
- return { width: w, height: h };
2529
- }
2530
2882
  function matrixFromComputed(el) {
2531
2883
  const tr = getComputedStyle(el).transform;
2532
2884
  if (!tr || tr === "none") return new DOMMatrix();
@@ -2558,24 +2910,33 @@ function bboxFromMatrix(w, h, M2D) {
2558
2910
  }
2559
2911
  function readIndividualTransforms(el) {
2560
2912
  const out = { rotate: "0deg", scale: null, translate: null };
2561
- const map = el.computedStyleMap?.();
2913
+ const map = typeof el.computedStyleMap === "function" ? el.computedStyleMap() : null;
2562
2914
  if (map) {
2563
- 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");
2564
2925
  if (rot) {
2565
2926
  const ang = rot.angle ?? rot;
2566
- if (ang && "unit" in ang) {
2927
+ if (ang && typeof ang === "object" && "unit" in ang) {
2567
2928
  out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2568
2929
  } else {
2569
2930
  out.rotate = String(rot);
2570
2931
  }
2571
2932
  }
2572
- const sc = map.get("scale");
2933
+ const sc = safeGet("scale");
2573
2934
  if (sc && sc.length) {
2574
2935
  const sx = sc[0]?.value ?? 1;
2575
2936
  const sy = sc[1]?.value ?? sx;
2576
2937
  out.scale = `${sx} ${sy}`;
2577
2938
  }
2578
- const tr = map.get("translate");
2939
+ const tr = safeGet("translate");
2579
2940
  if (tr && tr.length) {
2580
2941
  const tx = tr[0]?.value ?? 0;
2581
2942
  const ty = tr[1]?.value ?? 0;
@@ -2660,7 +3021,9 @@ function createContext(options = {}) {
2660
3021
  scale: options.scale ?? 1,
2661
3022
  // DOM filters
2662
3023
  exclude: options.exclude ?? [],
3024
+ excludeMode: options.excludeMode ?? "hide",
2663
3025
  filter: options.filter ?? null,
3026
+ filterMode: options.filterMode ?? "hide",
2664
3027
  placeholders: options.placeholders !== false,
2665
3028
  // default true
2666
3029
  // Fuentes
@@ -2668,7 +3031,7 @@ function createContext(options = {}) {
2668
3031
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2669
3032
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2670
3033
  excludeFonts: options.excludeFonts ?? void 0,
2671
- defaultImageUrl: options.defaultImageUrl ?? void 0,
3034
+ fallbackURL: options.fallbackURL ?? void 0,
2672
3035
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2673
3036
  /** @type {CachePolicy} */
2674
3037
  cache: cachePolicy,
@@ -2703,24 +3066,12 @@ async function toImg(url, options) {
2703
3066
  // src/exporters/toCanvas.js
2704
3067
  async function toCanvas(url, options) {
2705
3068
  const { scale, dpr } = options;
2706
- const safari = isSafari();
2707
3069
  const img = new Image();
2708
3070
  img.crossOrigin = "anonymous";
2709
3071
  img.loading = "eager";
2710
3072
  img.decoding = "sync";
2711
3073
  img.src = url;
2712
- let tempSlot = null;
2713
- if (safari) {
2714
- tempSlot = document.createElement("div");
2715
- tempSlot.setAttribute("aria-hidden", "true");
2716
- tempSlot.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;contain:size layout style;";
2717
- document.body.appendChild(tempSlot);
2718
- tempSlot.appendChild(img);
2719
- }
2720
3074
  await img.decode();
2721
- if (safari) {
2722
- await new Promise((r) => setTimeout(r, 100));
2723
- }
2724
3075
  const width = img.naturalWidth * scale;
2725
3076
  const height = img.naturalHeight * scale;
2726
3077
  const canvas = document.createElement("canvas");
@@ -2731,12 +3082,6 @@ async function toCanvas(url, options) {
2731
3082
  const ctx = canvas.getContext("2d");
2732
3083
  if (dpr !== 1) ctx.scale(dpr, dpr);
2733
3084
  ctx.drawImage(img, 0, 0, width, height);
2734
- if (tempSlot && tempSlot.parentNode) {
2735
- try {
2736
- tempSlot.parentNode.removeChild(tempSlot);
2737
- } catch {
2738
- }
2739
- }
2740
3085
  return canvas;
2741
3086
  }
2742
3087
 
@@ -2772,6 +3117,7 @@ async function rasterize(url, options) {
2772
3117
 
2773
3118
  // src/exporters/download.js
2774
3119
  async function download(url, options) {
3120
+ options.dpr = 1;
2775
3121
  if (options.format === "svg") {
2776
3122
  const blob = await toBlob(url, { ...options, type: "svg" });
2777
3123
  const objectURL = URL.createObjectURL(blob);
@@ -2792,8 +3138,19 @@ async function download(url, options) {
2792
3138
 
2793
3139
  // src/api/snapdom.js
2794
3140
  var INTERNAL_TOKEN = Symbol("snapdom.internal");
3141
+ var _safariWarmup = false;
2795
3142
  async function snapdom(element, userOptions) {
2796
3143
  if (!element) throw new Error("Element cannot be null or undefined");
3144
+ if (isSafari()) {
3145
+ for (let i = 0; i < 3; i++) {
3146
+ try {
3147
+ await safariWarmup(element, userOptions);
3148
+ console.log("Iteraci\xF3n n\xFAmero:", i);
3149
+ _safariWarmup = false;
3150
+ } catch (error) {
3151
+ }
3152
+ }
3153
+ }
2797
3154
  const context = createContext(userOptions);
2798
3155
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2799
3156
  if (!context.snap) {
@@ -2837,24 +3194,65 @@ snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then
2837
3194
  snapdom.toJpg = (el, options) => snapdom(el, { ...options, format: "jpeg" }).then((result) => result.toJpg());
2838
3195
  snapdom.toWebp = (el, options) => snapdom(el, { ...options, format: "webp" }).then((result) => result.toWebp());
2839
3196
  snapdom.download = (el, options) => snapdom(el, options).then((result) => result.download());
3197
+ async function safariWarmup(element, baseOptions) {
3198
+ if (_safariWarmup) return;
3199
+ const preflight = {
3200
+ ...baseOptions,
3201
+ fast: true,
3202
+ embedFonts: true,
3203
+ scale: 0.2
3204
+ };
3205
+ let url;
3206
+ try {
3207
+ url = await captureDOM(element, preflight);
3208
+ } catch {
3209
+ return;
3210
+ }
3211
+ await new Promise((resolve) => {
3212
+ const img = new Image();
3213
+ img.decoding = "sync";
3214
+ img.loading = "eager";
3215
+ img.style.position = "fixed";
3216
+ img.style.left = 0;
3217
+ img.style.top = 0;
3218
+ img.style.width = "10px";
3219
+ img.style.height = "10px";
3220
+ img.style.opacity = "0.01";
3221
+ img.style.transform = "translateZ(10px)";
3222
+ img.style.willChange = "transform,opacity;";
3223
+ img.src = url;
3224
+ const cleanup = async () => {
3225
+ await new Promise((r) => setTimeout(r, 100));
3226
+ if (img && img.parentNode) img.parentNode.removeChild(img);
3227
+ _safariWarmup = true;
3228
+ resolve();
3229
+ };
3230
+ document.body.appendChild(img);
3231
+ cleanup();
3232
+ });
3233
+ }
2840
3234
 
2841
3235
  // src/api/preCache.js
2842
3236
  async function preCache(root = document, options = {}) {
2843
3237
  const {
2844
3238
  embedFonts = true,
2845
- cacheOpt = "full",
2846
3239
  useProxy = ""
2847
3240
  } = options;
2848
- applyCachePolicy(cacheOpt);
3241
+ const cacheMode = options.cache ?? options.cacheOpt ?? "full";
3242
+ applyCachePolicy(cacheMode);
2849
3243
  try {
2850
- await document.fonts.ready;
3244
+ await document.fonts?.ready;
3245
+ } catch {
3246
+ }
3247
+ try {
3248
+ precacheCommonTags();
2851
3249
  } catch {
2852
3250
  }
2853
- precacheCommonTags();
2854
3251
  cache.session = cache.session || {};
2855
3252
  if (!cache.session.styleCache) {
2856
3253
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
2857
3254
  }
3255
+ cache.image = cache.image || /* @__PURE__ */ new Map();
2858
3256
  try {
2859
3257
  await inlineBackgroundImages(
2860
3258
  root,
@@ -2875,11 +3273,14 @@ async function preCache(root = document, options = {}) {
2875
3273
  }
2876
3274
  const promises = [];
2877
3275
  for (const img of imgEls) {
2878
- const src = img?.src;
3276
+ const src = img?.currentSrc || img?.src;
2879
3277
  if (!src) continue;
2880
3278
  if (!cache.image.has(src)) {
2881
- const p = Promise.resolve().then(() => snapFetch(src, { as: "dataURL", useProxy })).then((dataURL) => {
2882
- cache.image.set(src, dataURL);
3279
+ const p = Promise.resolve().then(async () => {
3280
+ const res = await snapFetch(src, { as: "dataURL", useProxy });
3281
+ if (res?.ok && typeof res.data === "string") {
3282
+ cache.image.set(src, res.data);
3283
+ }
2883
3284
  }).catch(() => {
2884
3285
  });
2885
3286
  promises.push(p);
@@ -2906,7 +3307,8 @@ async function preCache(root = document, options = {}) {
2906
3307
  try {
2907
3308
  const required = collectUsedFontVariants(root);
2908
3309
  const usedCodepoints = collectUsedCodepoints(root);
2909
- if (typeof isSafari === "function" ? isSafari() : !!isSafari) {
3310
+ const safari = typeof isSafari === "function" ? isSafari() : !!isSafari;
3311
+ if (safari) {
2910
3312
  const families = new Set(
2911
3313
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2912
3314
  );