@zumer/snapdom 1.9.11 → 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`;
@@ -898,19 +985,27 @@ async function deepClone(node, sessionCache, options) {
898
985
  return node.cloneNode(true);
899
986
  }
900
987
  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;
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
+ }
905
996
  }
906
997
  if (options.exclude && Array.isArray(options.exclude)) {
907
998
  for (const selector of options.exclude) {
908
999
  try {
909
1000
  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;
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
+ }
914
1009
  }
915
1010
  } catch (err) {
916
1011
  console.warn(`Invalid selector in exclude option: ${selector}`, err);
@@ -920,10 +1015,14 @@ async function deepClone(node, sessionCache, options) {
920
1015
  if (typeof options.filter === "function") {
921
1016
  try {
922
1017
  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;
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
+ }
927
1026
  }
928
1027
  } catch (err) {
929
1028
  console.warn("Error in filter function:", err);
@@ -2385,7 +2484,7 @@ async function captureDOM(element, options) {
2385
2484
  const families = new Set(
2386
2485
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2387
2486
  );
2388
- await ensureFontsReady(families, 5);
2487
+ await ensureFontsReady(families, 2);
2389
2488
  }
2390
2489
  fontsCSS = await embedCustomFonts({
2391
2490
  required,
@@ -2660,7 +2759,9 @@ function createContext(options = {}) {
2660
2759
  scale: options.scale ?? 1,
2661
2760
  // DOM filters
2662
2761
  exclude: options.exclude ?? [],
2762
+ excludeMode: options.excludeMode ?? "visuallyHide",
2663
2763
  filter: options.filter ?? null,
2764
+ filterMode: options.filterMode ?? "visuallyHide",
2664
2765
  placeholders: options.placeholders !== false,
2665
2766
  // default true
2666
2767
  // Fuentes
@@ -2703,24 +2804,12 @@ async function toImg(url, options) {
2703
2804
  // src/exporters/toCanvas.js
2704
2805
  async function toCanvas(url, options) {
2705
2806
  const { scale, dpr } = options;
2706
- const safari = isSafari();
2707
2807
  const img = new Image();
2708
2808
  img.crossOrigin = "anonymous";
2709
2809
  img.loading = "eager";
2710
2810
  img.decoding = "sync";
2711
2811
  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
2812
  await img.decode();
2721
- if (safari) {
2722
- await new Promise((r) => setTimeout(r, 100));
2723
- }
2724
2813
  const width = img.naturalWidth * scale;
2725
2814
  const height = img.naturalHeight * scale;
2726
2815
  const canvas = document.createElement("canvas");
@@ -2731,12 +2820,6 @@ async function toCanvas(url, options) {
2731
2820
  const ctx = canvas.getContext("2d");
2732
2821
  if (dpr !== 1) ctx.scale(dpr, dpr);
2733
2822
  ctx.drawImage(img, 0, 0, width, height);
2734
- if (tempSlot && tempSlot.parentNode) {
2735
- try {
2736
- tempSlot.parentNode.removeChild(tempSlot);
2737
- } catch {
2738
- }
2739
- }
2740
2823
  return canvas;
2741
2824
  }
2742
2825
 
@@ -2792,8 +2875,19 @@ async function download(url, options) {
2792
2875
 
2793
2876
  // src/api/snapdom.js
2794
2877
  var INTERNAL_TOKEN = Symbol("snapdom.internal");
2878
+ var _safariWarmup = false;
2795
2879
  async function snapdom(element, userOptions) {
2796
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
+ }
2797
2891
  const context = createContext(userOptions);
2798
2892
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2799
2893
  if (!context.snap) {
@@ -2837,24 +2931,65 @@ snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then
2837
2931
  snapdom.toJpg = (el, options) => snapdom(el, { ...options, format: "jpeg" }).then((result) => result.toJpg());
2838
2932
  snapdom.toWebp = (el, options) => snapdom(el, { ...options, format: "webp" }).then((result) => result.toWebp());
2839
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
+ }
2840
2971
 
2841
2972
  // src/api/preCache.js
2842
2973
  async function preCache(root = document, options = {}) {
2843
2974
  const {
2844
2975
  embedFonts = true,
2845
- cacheOpt = "full",
2846
2976
  useProxy = ""
2847
2977
  } = options;
2848
- applyCachePolicy(cacheOpt);
2978
+ const cacheMode = options.cache ?? options.cacheOpt ?? "full";
2979
+ applyCachePolicy(cacheMode);
2849
2980
  try {
2850
- await document.fonts.ready;
2981
+ await document.fonts?.ready;
2982
+ } catch {
2983
+ }
2984
+ try {
2985
+ precacheCommonTags();
2851
2986
  } catch {
2852
2987
  }
2853
- precacheCommonTags();
2854
2988
  cache.session = cache.session || {};
2855
2989
  if (!cache.session.styleCache) {
2856
2990
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
2857
2991
  }
2992
+ cache.image = cache.image || /* @__PURE__ */ new Map();
2858
2993
  try {
2859
2994
  await inlineBackgroundImages(
2860
2995
  root,
@@ -2875,11 +3010,14 @@ async function preCache(root = document, options = {}) {
2875
3010
  }
2876
3011
  const promises = [];
2877
3012
  for (const img of imgEls) {
2878
- const src = img?.src;
3013
+ const src = img?.currentSrc || img?.src;
2879
3014
  if (!src) continue;
2880
3015
  if (!cache.image.has(src)) {
2881
- const p = Promise.resolve().then(() => snapFetch(src, { as: "dataURL", useProxy })).then((dataURL) => {
2882
- 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
+ }
2883
3021
  }).catch(() => {
2884
3022
  });
2885
3023
  promises.push(p);
@@ -2906,7 +3044,8 @@ async function preCache(root = document, options = {}) {
2906
3044
  try {
2907
3045
  const required = collectUsedFontVariants(root);
2908
3046
  const usedCodepoints = collectUsedCodepoints(root);
2909
- if (typeof isSafari === "function" ? isSafari() : !!isSafari) {
3047
+ const safari = typeof isSafari === "function" ? isSafari() : !!isSafari;
3048
+ if (safari) {
2910
3049
  const families = new Set(
2911
3050
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2912
3051
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.9.11",
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