@zumer/snapdom 1.9.10 → 1.9.12-dev.0

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.0
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`;
@@ -670,22 +757,14 @@ function idleCallback(childList, callback, fast) {
670
757
  return Promise.all(childList.map((child) => {
671
758
  return new Promise((resolve) => {
672
759
  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
760
+ idle((deadline) => {
761
+ const hasIdleBudget = deadline && typeof deadline.timeRemaining === "function" ? deadline.timeRemaining() > 0 : true;
762
+ if (hasIdleBudget) {
763
+ callback(child, resolve);
764
+ } else {
765
+ deal();
687
766
  }
688
- );
767
+ }, { fast });
689
768
  }
690
769
  deal();
691
770
  });
@@ -906,19 +985,27 @@ async function deepClone(node, sessionCache, options) {
906
985
  return node.cloneNode(true);
907
986
  }
908
987
  if (node.getAttribute("data-capture") === "exclude") {
909
- const spacer = document.createElement("div");
910
- const rect = node.getBoundingClientRect();
911
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
912
- return spacer;
988
+ if (options.excludeMode === "visuallyHide") {
989
+ const spacer = document.createElement("div");
990
+ const rect = node.getBoundingClientRect();
991
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
992
+ return spacer;
993
+ } else if (options.excludeMode === "remove") {
994
+ return null;
995
+ }
913
996
  }
914
997
  if (options.exclude && Array.isArray(options.exclude)) {
915
998
  for (const selector of options.exclude) {
916
999
  try {
917
1000
  if (node.matches?.(selector)) {
918
- const spacer = document.createElement("div");
919
- const rect = node.getBoundingClientRect();
920
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
921
- return spacer;
1001
+ if (options.excludeMode === "visuallyHide") {
1002
+ const spacer = document.createElement("div");
1003
+ const rect = node.getBoundingClientRect();
1004
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
1005
+ return spacer;
1006
+ } else if (options.excludeMode === "remove") {
1007
+ return null;
1008
+ }
922
1009
  }
923
1010
  } catch (err) {
924
1011
  console.warn(`Invalid selector in exclude option: ${selector}`, err);
@@ -928,10 +1015,14 @@ async function deepClone(node, sessionCache, options) {
928
1015
  if (typeof options.filter === "function") {
929
1016
  try {
930
1017
  if (!options.filter(node)) {
931
- const spacer = document.createElement("div");
932
- const rect = node.getBoundingClientRect();
933
- spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
934
- return spacer;
1018
+ if (options.filterMode === "visuallyHide") {
1019
+ const spacer = document.createElement("div");
1020
+ const rect = node.getBoundingClientRect();
1021
+ spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
1022
+ return spacer;
1023
+ } else if (options.filterMode === "remove") {
1024
+ return null;
1025
+ }
935
1026
  }
936
1027
  } catch (err) {
937
1028
  console.warn("Error in filter function:", err);
@@ -1774,7 +1865,7 @@ function collectUsedCodepoints(root) {
1774
1865
  }
1775
1866
  return used;
1776
1867
  }
1777
- async function ensureFontsReady(families, warmupRepetitions = 1) {
1868
+ async function ensureFontsReady(families, warmupRepetitions = 2) {
1778
1869
  try {
1779
1870
  await document.fonts.ready;
1780
1871
  } catch {
@@ -2668,7 +2759,9 @@ function createContext(options = {}) {
2668
2759
  scale: options.scale ?? 1,
2669
2760
  // DOM filters
2670
2761
  exclude: options.exclude ?? [],
2762
+ excludeMode: options.excludeMode ?? "visuallyHide",
2671
2763
  filter: options.filter ?? null,
2764
+ filterMode: options.filterMode ?? "visuallyHide",
2672
2765
  placeholders: options.placeholders !== false,
2673
2766
  // default true
2674
2767
  // Fuentes
@@ -2711,24 +2804,12 @@ async function toImg(url, options) {
2711
2804
  // src/exporters/toCanvas.js
2712
2805
  async function toCanvas(url, options) {
2713
2806
  const { scale, dpr } = options;
2714
- const safari = isSafari();
2715
2807
  const img = new Image();
2716
2808
  img.crossOrigin = "anonymous";
2717
2809
  img.loading = "eager";
2718
2810
  img.decoding = "sync";
2719
2811
  img.src = url;
2720
- let tempSlot = null;
2721
- if (safari) {
2722
- tempSlot = document.createElement("div");
2723
- tempSlot.setAttribute("aria-hidden", "true");
2724
- tempSlot.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;contain:size layout style;";
2725
- document.body.appendChild(tempSlot);
2726
- tempSlot.appendChild(img);
2727
- }
2728
2812
  await img.decode();
2729
- if (safari) {
2730
- await new Promise((r) => setTimeout(r, 100));
2731
- }
2732
2813
  const width = img.naturalWidth * scale;
2733
2814
  const height = img.naturalHeight * scale;
2734
2815
  const canvas = document.createElement("canvas");
@@ -2739,12 +2820,6 @@ async function toCanvas(url, options) {
2739
2820
  const ctx = canvas.getContext("2d");
2740
2821
  if (dpr !== 1) ctx.scale(dpr, dpr);
2741
2822
  ctx.drawImage(img, 0, 0, width, height);
2742
- if (tempSlot && tempSlot.parentNode) {
2743
- try {
2744
- tempSlot.parentNode.removeChild(tempSlot);
2745
- } catch {
2746
- }
2747
- }
2748
2823
  return canvas;
2749
2824
  }
2750
2825
 
@@ -2800,8 +2875,19 @@ async function download(url, options) {
2800
2875
 
2801
2876
  // src/api/snapdom.js
2802
2877
  var INTERNAL_TOKEN = Symbol("snapdom.internal");
2878
+ var _safariWarmup = false;
2803
2879
  async function snapdom(element, userOptions) {
2804
2880
  if (!element) throw new Error("Element cannot be null or undefined");
2881
+ if (isSafari()) {
2882
+ for (let i = 0; i < 3; i++) {
2883
+ try {
2884
+ await safariWarmup(element, userOptions);
2885
+ console.log("Iteraci\xF3n n\xFAmero:", i);
2886
+ _safariWarmup = false;
2887
+ } catch (error) {
2888
+ }
2889
+ }
2890
+ }
2805
2891
  const context = createContext(userOptions);
2806
2892
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2807
2893
  if (!context.snap) {
@@ -2845,24 +2931,65 @@ snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then
2845
2931
  snapdom.toJpg = (el, options) => snapdom(el, { ...options, format: "jpeg" }).then((result) => result.toJpg());
2846
2932
  snapdom.toWebp = (el, options) => snapdom(el, { ...options, format: "webp" }).then((result) => result.toWebp());
2847
2933
  snapdom.download = (el, options) => snapdom(el, options).then((result) => result.download());
2934
+ async function safariWarmup(element, baseOptions) {
2935
+ if (_safariWarmup) return;
2936
+ const preflight = {
2937
+ ...baseOptions,
2938
+ fast: true,
2939
+ embedFonts: true,
2940
+ scale: 0.2
2941
+ };
2942
+ let url;
2943
+ try {
2944
+ url = await captureDOM(element, preflight);
2945
+ } catch {
2946
+ return;
2947
+ }
2948
+ await new Promise((resolve) => {
2949
+ const img = new Image();
2950
+ img.decoding = "sync";
2951
+ img.loading = "eager";
2952
+ img.style.position = "fixed";
2953
+ img.style.left = 0;
2954
+ img.style.top = 0;
2955
+ img.style.width = "10px";
2956
+ img.style.height = "10px";
2957
+ img.style.opacity = "0.01";
2958
+ img.style.transform = "translateZ(10px)";
2959
+ img.style.willChange = "transform,opacity;";
2960
+ img.src = url;
2961
+ const cleanup = async () => {
2962
+ await new Promise((r) => setTimeout(r, 100));
2963
+ if (img && img.parentNode) img.parentNode.removeChild(img);
2964
+ _safariWarmup = true;
2965
+ resolve();
2966
+ };
2967
+ document.body.appendChild(img);
2968
+ cleanup();
2969
+ });
2970
+ }
2848
2971
 
2849
2972
  // src/api/preCache.js
2850
2973
  async function preCache(root = document, options = {}) {
2851
2974
  const {
2852
2975
  embedFonts = true,
2853
- cacheOpt = "full",
2854
2976
  useProxy = ""
2855
2977
  } = options;
2856
- applyCachePolicy(cacheOpt);
2978
+ const cacheMode = options.cache ?? options.cacheOpt ?? "full";
2979
+ applyCachePolicy(cacheMode);
2857
2980
  try {
2858
- await document.fonts.ready;
2981
+ await document.fonts?.ready;
2982
+ } catch {
2983
+ }
2984
+ try {
2985
+ precacheCommonTags();
2859
2986
  } catch {
2860
2987
  }
2861
- precacheCommonTags();
2862
2988
  cache.session = cache.session || {};
2863
2989
  if (!cache.session.styleCache) {
2864
2990
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
2865
2991
  }
2992
+ cache.image = cache.image || /* @__PURE__ */ new Map();
2866
2993
  try {
2867
2994
  await inlineBackgroundImages(
2868
2995
  root,
@@ -2883,11 +3010,14 @@ async function preCache(root = document, options = {}) {
2883
3010
  }
2884
3011
  const promises = [];
2885
3012
  for (const img of imgEls) {
2886
- const src = img?.src;
3013
+ const src = img?.currentSrc || img?.src;
2887
3014
  if (!src) continue;
2888
3015
  if (!cache.image.has(src)) {
2889
- const p = Promise.resolve().then(() => snapFetch(src, { as: "dataURL", useProxy })).then((dataURL) => {
2890
- cache.image.set(src, dataURL);
3016
+ const p = Promise.resolve().then(async () => {
3017
+ const res = await snapFetch(src, { as: "dataURL", useProxy });
3018
+ if (res?.ok && typeof res.data === "string") {
3019
+ cache.image.set(src, res.data);
3020
+ }
2891
3021
  }).catch(() => {
2892
3022
  });
2893
3023
  promises.push(p);
@@ -2914,7 +3044,8 @@ async function preCache(root = document, options = {}) {
2914
3044
  try {
2915
3045
  const required = collectUsedFontVariants(root);
2916
3046
  const usedCodepoints = collectUsedCodepoints(root);
2917
- if (typeof isSafari === "function" ? isSafari() : !!isSafari) {
3047
+ const safari = typeof isSafari === "function" ? isSafari() : !!isSafari;
3048
+ if (safari) {
2918
3049
  const families = new Set(
2919
3050
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2920
3051
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.9.10",
3
+ "version": "1.9.12-dev.0",
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,6 +13,9 @@ 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';
18
+
16
19
  export interface LocalFontDescriptor {
17
20
  /** CSS font-family name (e.g. "Inter"). */
18
21
  family: string;
@@ -87,12 +90,24 @@ export interface CaptureOptions {
87
90
  /** CSS selectors removed from the cloned subtree before processing. */
88
91
  exclude?: string[];
89
92
 
93
+ /**
94
+ * Mode applied to excluded nodes of the cloned tree.
95
+ * Default: "visuallyHide"
96
+ */
97
+ excludeMode?: ExcludeMode;
98
+
90
99
  /**
91
100
  * Advanced node filter; return false to exclude a node during traversal.
92
101
  * Applied to the cloned subtree.
93
102
  */
94
103
  filter?: (el: Element) => boolean;
95
104
 
105
+ /**
106
+ * Mode applied to filtered nodes of the cloned tree.
107
+ * Default: "visuallyHide"
108
+ */
109
+ filterMode?: FilterMode;
110
+
96
111
  /**
97
112
  * Whether to synthesize placeholders for broken images, etc.
98
113
  * Default: true