@zumer/snapdom 1.9.8 → 1.9.9
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/README.md +6 -2
- package/dist/snapdom.js +478 -182
- package/dist/snapdom.min.js +3 -3
- package/dist/snapdom.min.mjs +3 -3
- package/dist/snapdom.mjs +464 -180
- package/package.json +1 -1
- package/types/snapdom.d.ts +3 -0
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.
|
|
4
|
+
* v.1.9.9
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -18,18 +18,10 @@ var cache = {
|
|
|
18
18
|
font: /* @__PURE__ */ new Set(),
|
|
19
19
|
snapshot: /* @__PURE__ */ new WeakMap(),
|
|
20
20
|
snapshotKey: /* @__PURE__ */ new Map(),
|
|
21
|
-
preStyleMap: /* @__PURE__ */ new Map(),
|
|
22
|
-
preStyle: /* @__PURE__ */ new WeakMap(),
|
|
23
|
-
preNodeMap: /* @__PURE__ */ new Map(),
|
|
24
21
|
reset: resetCache
|
|
25
22
|
};
|
|
26
23
|
function resetCache() {
|
|
27
24
|
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
28
|
-
cache.snapshot = /* @__PURE__ */ new WeakMap();
|
|
29
|
-
cache.snapshotKey.clear();
|
|
30
|
-
cache.preStyleMap.clear();
|
|
31
|
-
cache.preStyle = /* @__PURE__ */ new WeakMap();
|
|
32
|
-
cache.preNodeMap.clear();
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
// src/utils/cssTools.js
|
|
@@ -163,8 +155,8 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
163
155
|
}
|
|
164
156
|
return css;
|
|
165
157
|
}
|
|
166
|
-
function generateCSSClasses() {
|
|
167
|
-
const keySet = new Set(
|
|
158
|
+
function generateCSSClasses(styleMap) {
|
|
159
|
+
const keySet = new Set(styleMap.values());
|
|
168
160
|
const classMap = /* @__PURE__ */ new Map();
|
|
169
161
|
let counter = 1;
|
|
170
162
|
for (const key of keySet) {
|
|
@@ -234,7 +226,25 @@ function extractURL(value) {
|
|
|
234
226
|
if (url.startsWith("#")) return null;
|
|
235
227
|
return url;
|
|
236
228
|
}
|
|
237
|
-
function
|
|
229
|
+
async function fetchResource(url, { useProxy = "" } = {}) {
|
|
230
|
+
async function doFetch(u) {
|
|
231
|
+
const res = await fetch(u);
|
|
232
|
+
if (!res.ok) throw new Error(`[snapdom] Failed to fetch resource: ${u}`);
|
|
233
|
+
return res;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
return await doFetch(url);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
if (useProxy && typeof useProxy === "string") {
|
|
239
|
+
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
240
|
+
return doFetch(proxied);
|
|
241
|
+
}
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
var _inflight = /* @__PURE__ */ new Map();
|
|
246
|
+
var _errorCache = /* @__PURE__ */ new Map();
|
|
247
|
+
function fetchImage(src, { timeout = 3e3, useProxy = "", errorTTL = 8e3 } = {}) {
|
|
238
248
|
function getCrossOriginMode(url) {
|
|
239
249
|
try {
|
|
240
250
|
const parsed = new URL(url, window.location.href);
|
|
@@ -243,105 +253,157 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
243
253
|
return "anonymous";
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
250
|
-
}).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
|
|
251
|
-
const reader = new FileReader();
|
|
252
|
-
reader.onloadend = () => {
|
|
253
|
-
const base64 = reader.result;
|
|
254
|
-
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
255
|
-
reject(new Error("Invalid image data URL"));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
resolve(base64);
|
|
259
|
-
};
|
|
260
|
-
reader.onerror = () => reject(new Error("FileReader error"));
|
|
261
|
-
reader.readAsDataURL(blob);
|
|
262
|
-
}));
|
|
256
|
+
const ok = (data) => ({ ok: true, data });
|
|
257
|
+
const fail = (e) => ({ ok: false, error: e instanceof Error ? e : new Error(String(e)) });
|
|
258
|
+
function fetchBlobAsDataURLSafe(fetchUrl) {
|
|
263
259
|
try {
|
|
264
|
-
return
|
|
260
|
+
return fetch(fetchUrl, {
|
|
261
|
+
mode: "cors",
|
|
262
|
+
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
263
|
+
}).then((r) => {
|
|
264
|
+
if (!r.ok) return fail(new Error("HTTP " + r.status));
|
|
265
|
+
return r.blob().then((blob) => new Promise((resolve) => {
|
|
266
|
+
const reader = new FileReader();
|
|
267
|
+
reader.onloadend = () => {
|
|
268
|
+
const base64 = reader.result;
|
|
269
|
+
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
270
|
+
resolve(fail(new Error("Invalid image data URL")));
|
|
271
|
+
} else {
|
|
272
|
+
resolve(ok(base64));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
reader.onerror = () => resolve(fail(new Error("FileReader error")));
|
|
276
|
+
reader.readAsDataURL(blob);
|
|
277
|
+
}));
|
|
278
|
+
}).catch((e) => fail(e));
|
|
265
279
|
} catch (e) {
|
|
280
|
+
return Promise.resolve(fail(e));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function fetchWithFallbackOnceSafe(url) {
|
|
284
|
+
return fetchBlobAsDataURLSafe(url).then((r) => {
|
|
285
|
+
if (r.ok) return r;
|
|
266
286
|
if (useProxy && typeof useProxy === "string") {
|
|
267
287
|
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
throw new Error("[SnapDOM - fetchImage] Fetch fallback failed and no proxy provided");
|
|
288
|
+
return fetchBlobAsDataURLSafe(proxied).then((r2) => {
|
|
289
|
+
if (r2.ok) return r2;
|
|
290
|
+
return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
|
|
291
|
+
});
|
|
275
292
|
}
|
|
276
|
-
|
|
293
|
+
return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
|
|
294
|
+
});
|
|
277
295
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
const until = _errorCache.get(src);
|
|
298
|
+
if (until && until > now) {
|
|
299
|
+
const pr = Promise.reject(new Error("[SnapDOM - fetchImage] Recently failed (cooldown)."));
|
|
300
|
+
pr.catch(() => {
|
|
301
|
+
});
|
|
302
|
+
return pr;
|
|
281
303
|
}
|
|
282
|
-
|
|
283
|
-
|
|
304
|
+
if (_inflight.has(src)) return _inflight.get(src);
|
|
305
|
+
const crossOriginValue = getCrossOriginMode(src);
|
|
306
|
+
if (cache.image.has(src)) return Promise.resolve(cache.image.get(src));
|
|
307
|
+
if (src.startsWith("data:image/")) {
|
|
284
308
|
cache.image.set(src, src);
|
|
285
309
|
return Promise.resolve(src);
|
|
286
310
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
const response = await fetch(src, {
|
|
292
|
-
mode: "cors",
|
|
293
|
-
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
294
|
-
});
|
|
295
|
-
const svgText = await response.text();
|
|
296
|
-
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
297
|
-
cache.image.set(src, encoded);
|
|
298
|
-
return encoded;
|
|
299
|
-
} catch {
|
|
300
|
-
return fetchWithFallback(src);
|
|
301
|
-
}
|
|
302
|
-
})();
|
|
303
|
-
}
|
|
304
|
-
return new Promise((resolve, reject) => {
|
|
305
|
-
const timeoutId = setTimeout(() => {
|
|
306
|
-
reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
|
|
307
|
-
}, timeout);
|
|
308
|
-
const image = new Image();
|
|
309
|
-
image.crossOrigin = crossOriginValue;
|
|
310
|
-
image.onload = async () => {
|
|
311
|
-
clearTimeout(timeoutId);
|
|
312
|
-
try {
|
|
313
|
-
await image.decode();
|
|
314
|
-
const canvas = document.createElement("canvas");
|
|
315
|
-
canvas.width = image.width;
|
|
316
|
-
canvas.height = image.height;
|
|
317
|
-
const ctx = canvas.getContext("2d");
|
|
318
|
-
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
319
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
320
|
-
cache.image.set(src, dataURL);
|
|
321
|
-
resolve(dataURL);
|
|
322
|
-
} catch {
|
|
311
|
+
if (/\.svg(\?.*)?$/i.test(src)) {
|
|
312
|
+
const p2 = (async () => {
|
|
313
|
+
const direct = await (async () => {
|
|
323
314
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
315
|
+
const res = await fetch(src, {
|
|
316
|
+
mode: "cors",
|
|
317
|
+
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
318
|
+
});
|
|
319
|
+
if (!res.ok) return fail(new Error("HTTP " + res.status));
|
|
320
|
+
const svgText = await res.text();
|
|
321
|
+
return ok(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`);
|
|
327
322
|
} catch (e) {
|
|
328
|
-
|
|
323
|
+
return fail(e);
|
|
329
324
|
}
|
|
325
|
+
})();
|
|
326
|
+
if (direct.ok) {
|
|
327
|
+
cache.image.set(src, direct.data);
|
|
328
|
+
return direct.data;
|
|
330
329
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const fallbackDataURL = await fetchWithFallback(src);
|
|
337
|
-
cache.image.set(src, fallbackDataURL);
|
|
338
|
-
resolve(fallbackDataURL);
|
|
339
|
-
} catch (e) {
|
|
340
|
-
reject(e);
|
|
330
|
+
const via = await fetchWithFallbackOnceSafe(src);
|
|
331
|
+
if (via.ok) {
|
|
332
|
+
cache.image.set(src, via.data);
|
|
333
|
+
return via.data;
|
|
341
334
|
}
|
|
335
|
+
_errorCache.set(src, now + errorTTL);
|
|
336
|
+
return Promise.reject(via.error);
|
|
337
|
+
})();
|
|
338
|
+
_inflight.set(src, p2);
|
|
339
|
+
p2.finally(() => _inflight.delete(src));
|
|
340
|
+
p2.catch(() => {
|
|
341
|
+
});
|
|
342
|
+
return p2;
|
|
343
|
+
}
|
|
344
|
+
const p = new Promise((resolve, reject) => {
|
|
345
|
+
let finished = false;
|
|
346
|
+
const img = new Image();
|
|
347
|
+
const finish = (fn) => (arg) => {
|
|
348
|
+
if (finished) return;
|
|
349
|
+
finished = true;
|
|
350
|
+
clearTimeout(timeoutId);
|
|
351
|
+
img.onload = img.onerror = null;
|
|
352
|
+
fn(arg);
|
|
353
|
+
};
|
|
354
|
+
const onSuccess = (d) => {
|
|
355
|
+
cache.image.set(src, d);
|
|
356
|
+
resolve(d);
|
|
342
357
|
};
|
|
343
|
-
|
|
358
|
+
const onFinalError = (e) => {
|
|
359
|
+
_errorCache.set(src, Date.now() + errorTTL);
|
|
360
|
+
reject(e);
|
|
361
|
+
};
|
|
362
|
+
const timeoutId = setTimeout(
|
|
363
|
+
finish(() => {
|
|
364
|
+
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
365
|
+
if (r.ok) onSuccess(r.data);
|
|
366
|
+
else onFinalError(new Error("Image load timed out"));
|
|
367
|
+
});
|
|
368
|
+
}),
|
|
369
|
+
timeout
|
|
370
|
+
);
|
|
371
|
+
img.crossOrigin = crossOriginValue;
|
|
372
|
+
img.onload = finish(() => {
|
|
373
|
+
Promise.resolve(img.decode()).then(() => {
|
|
374
|
+
try {
|
|
375
|
+
const canvas = document.createElement("canvas");
|
|
376
|
+
canvas.width = img.naturalWidth || img.width;
|
|
377
|
+
canvas.height = img.naturalHeight || img.height;
|
|
378
|
+
const ctx = canvas.getContext("2d");
|
|
379
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
380
|
+
onSuccess(canvas.toDataURL("image/png"));
|
|
381
|
+
} catch {
|
|
382
|
+
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
383
|
+
if (r.ok) onSuccess(r.data);
|
|
384
|
+
else onFinalError(r.error);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}).catch(() => {
|
|
388
|
+
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
389
|
+
if (r.ok) onSuccess(r.data);
|
|
390
|
+
else onFinalError(r.error);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
img.onerror = finish(() => {
|
|
395
|
+
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
396
|
+
if (r.ok) onSuccess(r.data);
|
|
397
|
+
else onFinalError(r.error);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
img.src = src;
|
|
344
401
|
});
|
|
402
|
+
_inflight.set(src, p);
|
|
403
|
+
p.finally(() => _inflight.delete(src));
|
|
404
|
+
p.catch(() => {
|
|
405
|
+
});
|
|
406
|
+
return p;
|
|
345
407
|
}
|
|
346
408
|
function snapshotComputedStyle(style) {
|
|
347
409
|
const snap = {};
|
|
@@ -398,6 +460,8 @@ function splitBackgroundImage(bg) {
|
|
|
398
460
|
}
|
|
399
461
|
|
|
400
462
|
// src/modules/styles.js
|
|
463
|
+
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
464
|
+
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
401
465
|
function snapshotComputedStyleFull(style) {
|
|
402
466
|
const result = {};
|
|
403
467
|
const computedVisibility = style.getPropertyValue("visibility");
|
|
@@ -414,30 +478,42 @@ function snapshotComputedStyleFull(style) {
|
|
|
414
478
|
}
|
|
415
479
|
return result;
|
|
416
480
|
}
|
|
417
|
-
function inlineAllStyles(source, clone, compress) {
|
|
481
|
+
function inlineAllStyles(source, clone, styleMap, cache2, compress) {
|
|
418
482
|
if (source.tagName === "STYLE") return;
|
|
419
|
-
if (!
|
|
420
|
-
|
|
483
|
+
if (!cache2.has(source)) {
|
|
484
|
+
cache2.set(source, getStyle(source));
|
|
421
485
|
}
|
|
422
|
-
const style =
|
|
423
|
-
if (!
|
|
486
|
+
const style = cache2.get(source);
|
|
487
|
+
if (!snapshotCache.has(source)) {
|
|
424
488
|
const snapshot2 = snapshotComputedStyleFull(style);
|
|
425
|
-
|
|
489
|
+
snapshotCache.set(source, snapshot2);
|
|
426
490
|
}
|
|
427
|
-
const snapshot =
|
|
491
|
+
const snapshot = snapshotCache.get(source);
|
|
428
492
|
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
429
|
-
if (
|
|
430
|
-
|
|
493
|
+
if (snapshotKeyCache.has(hash)) {
|
|
494
|
+
styleMap.set(clone, snapshotKeyCache.get(hash));
|
|
431
495
|
return;
|
|
432
496
|
}
|
|
433
497
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
434
498
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
435
|
-
|
|
436
|
-
|
|
499
|
+
snapshotKeyCache.set(hash, key);
|
|
500
|
+
styleMap.set(clone, key);
|
|
437
501
|
}
|
|
438
502
|
|
|
439
503
|
// src/core/clone.js
|
|
440
|
-
function
|
|
504
|
+
function freezeImgSrcset(original, cloned) {
|
|
505
|
+
try {
|
|
506
|
+
const chosen = original.currentSrc || original.src || "";
|
|
507
|
+
if (!chosen) return;
|
|
508
|
+
cloned.setAttribute("src", chosen);
|
|
509
|
+
cloned.removeAttribute("srcset");
|
|
510
|
+
cloned.removeAttribute("sizes");
|
|
511
|
+
cloned.loading = "eager";
|
|
512
|
+
cloned.decoding = "sync";
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
|
|
441
517
|
if (!node) throw new Error("Invalid node");
|
|
442
518
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
443
519
|
let pendingSelectValue = null;
|
|
@@ -486,8 +562,8 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
486
562
|
}
|
|
487
563
|
if (node.getAttribute("data-capture") === "placeholder") {
|
|
488
564
|
const clone2 = node.cloneNode(false);
|
|
489
|
-
|
|
490
|
-
inlineAllStyles(node, clone2, compress);
|
|
565
|
+
nodeMap.set(clone2, node);
|
|
566
|
+
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
491
567
|
const placeholder = document.createElement("div");
|
|
492
568
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
493
569
|
placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
|
|
@@ -500,14 +576,17 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
500
576
|
img.src = dataURL;
|
|
501
577
|
img.width = node.width;
|
|
502
578
|
img.height = node.height;
|
|
503
|
-
|
|
504
|
-
inlineAllStyles(node, img, compress);
|
|
579
|
+
nodeMap.set(img, node);
|
|
580
|
+
inlineAllStyles(node, img, styleMap, styleCache, compress);
|
|
505
581
|
return img;
|
|
506
582
|
}
|
|
507
583
|
let clone;
|
|
508
584
|
try {
|
|
509
585
|
clone = node.cloneNode(false);
|
|
510
|
-
|
|
586
|
+
nodeMap.set(clone, node);
|
|
587
|
+
if (node.tagName === "IMG") {
|
|
588
|
+
freezeImgSrcset(node, clone);
|
|
589
|
+
}
|
|
511
590
|
} catch (err) {
|
|
512
591
|
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
513
592
|
throw err;
|
|
@@ -532,7 +611,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
532
611
|
if (node instanceof HTMLSelectElement) {
|
|
533
612
|
pendingSelectValue = node.value;
|
|
534
613
|
}
|
|
535
|
-
inlineAllStyles(node, clone, compress);
|
|
614
|
+
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
536
615
|
if (node.shadowRoot) {
|
|
537
616
|
const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
|
|
538
617
|
if (hasSlot) {
|
|
@@ -540,8 +619,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
540
619
|
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
541
620
|
const cssText = child.textContent || "";
|
|
542
621
|
if (cssText.trim() && compress) {
|
|
543
|
-
|
|
544
|
-
cache.preStyle.set(child, cssText);
|
|
622
|
+
styleCache.set(child, cssText);
|
|
545
623
|
}
|
|
546
624
|
}
|
|
547
625
|
}
|
|
@@ -551,12 +629,11 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
551
629
|
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
552
630
|
const cssText = child.textContent || "";
|
|
553
631
|
if (cssText.trim() && compress) {
|
|
554
|
-
|
|
555
|
-
cache.preStyle.set(child, cssText);
|
|
632
|
+
styleCache.set(child, cssText);
|
|
556
633
|
}
|
|
557
634
|
continue;
|
|
558
635
|
}
|
|
559
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
636
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
560
637
|
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
561
638
|
}
|
|
562
639
|
clone.appendChild(shadowFrag);
|
|
@@ -567,14 +644,14 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
567
644
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
568
645
|
const fragment = document.createDocumentFragment();
|
|
569
646
|
for (const child of nodesToClone) {
|
|
570
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
647
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
571
648
|
if (clonedChild) fragment.appendChild(clonedChild);
|
|
572
649
|
}
|
|
573
650
|
return fragment;
|
|
574
651
|
}
|
|
575
652
|
for (const child of node.childNodes) {
|
|
576
653
|
if (clonedAssignedNodes.has(child)) continue;
|
|
577
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
654
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
578
655
|
if (clonedChild) clone.appendChild(clonedChild);
|
|
579
656
|
}
|
|
580
657
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
@@ -679,7 +756,7 @@ function injectLinkIfMissing(href) {
|
|
|
679
756
|
document.head.appendChild(link);
|
|
680
757
|
});
|
|
681
758
|
}
|
|
682
|
-
async function embedCustomFonts({ preCached = false } = {}) {
|
|
759
|
+
async function embedCustomFonts({ preCached = false, localFonts = [], useProxy = "" } = {}) {
|
|
683
760
|
if (cache.resource.has("fonts-embed-css")) {
|
|
684
761
|
if (preCached) {
|
|
685
762
|
const style = document.createElement("style");
|
|
@@ -689,6 +766,15 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
689
766
|
}
|
|
690
767
|
return cache.resource.get("fonts-embed-css");
|
|
691
768
|
}
|
|
769
|
+
const loadedFonts = /* @__PURE__ */ new Set();
|
|
770
|
+
try {
|
|
771
|
+
for (const f of document.fonts) {
|
|
772
|
+
if (f.status === "loaded") {
|
|
773
|
+
loadedFonts.add(`${f.family}__${f.weight || "normal"}__${f.style || "normal"}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
692
778
|
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
693
779
|
const styleImports = [];
|
|
694
780
|
for (const styleTag of document.querySelectorAll("style")) {
|
|
@@ -707,26 +793,48 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
707
793
|
let finalCSS = "";
|
|
708
794
|
for (const link of links) {
|
|
709
795
|
try {
|
|
710
|
-
const res = await
|
|
796
|
+
const res = await fetchResource(link.href, { useProxy });
|
|
711
797
|
const cssText = await res.text();
|
|
712
798
|
if (isIconFont(link.href) || isIconFont(cssText)) continue;
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
799
|
+
const faceRegex = /@font-face[^{}]*{[^}]*}/g;
|
|
800
|
+
let cssFinal = cssText;
|
|
801
|
+
for (const face of cssText.match(faceRegex) || []) {
|
|
802
|
+
const famMatch = face.match(/font-family:\s*([^;]+);/i);
|
|
803
|
+
if (!famMatch) continue;
|
|
804
|
+
const family = famMatch[1].replace(/['"]/g, "").trim();
|
|
805
|
+
const weightMatch = face.match(/font-weight:\s*([^;]+);/i);
|
|
806
|
+
const styleMatch = face.match(/font-style:\s*([^;]+);/i);
|
|
807
|
+
const weight = weightMatch ? weightMatch[1].trim() : "normal";
|
|
808
|
+
const style = styleMatch ? styleMatch[1].trim() : "normal";
|
|
809
|
+
const key = `${family}__${weight}__${style}`;
|
|
810
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
811
|
+
const hasURL = /url\(/i.test(face);
|
|
812
|
+
const hasLocal = /local\(/i.test(face);
|
|
813
|
+
if (!hasURL && hasLocal) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (!loadedFonts.has(key)) {
|
|
817
|
+
cssFinal = cssFinal.replace(face, "");
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
let inlined = face;
|
|
821
|
+
const matches = Array.from(face.matchAll(urlRegex));
|
|
822
|
+
for (const match of matches) {
|
|
716
823
|
let rawUrl = extractURL(match[0]);
|
|
717
|
-
if (!rawUrl)
|
|
824
|
+
if (!rawUrl) continue;
|
|
718
825
|
let url = rawUrl;
|
|
719
826
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
720
827
|
url = new URL(url, link.href).href;
|
|
721
828
|
}
|
|
722
|
-
if (isIconFont(url))
|
|
829
|
+
if (isIconFont(url)) continue;
|
|
723
830
|
if (cache.resource.has(url)) {
|
|
724
831
|
cache.font.add(url);
|
|
725
|
-
|
|
832
|
+
inlined = inlined.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
833
|
+
continue;
|
|
726
834
|
}
|
|
727
|
-
if (cache.font.has(url))
|
|
835
|
+
if (cache.font.has(url)) continue;
|
|
728
836
|
try {
|
|
729
|
-
const fontRes = await
|
|
837
|
+
const fontRes = await fetchResource(url, { useProxy });
|
|
730
838
|
const blob = await fontRes.blob();
|
|
731
839
|
const b64 = await new Promise((resolve) => {
|
|
732
840
|
const reader = new FileReader();
|
|
@@ -735,16 +843,12 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
735
843
|
});
|
|
736
844
|
cache.resource.set(url, b64);
|
|
737
845
|
cache.font.add(url);
|
|
738
|
-
|
|
846
|
+
inlined = inlined.replace(match[0], `url(${b64})`);
|
|
739
847
|
} catch (e) {
|
|
740
848
|
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
741
|
-
return null;
|
|
742
849
|
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
let cssFinal = cssText;
|
|
746
|
-
for (const r of inlinedCSS) {
|
|
747
|
-
if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
850
|
+
}
|
|
851
|
+
cssFinal = cssFinal.replace(face, inlined);
|
|
748
852
|
}
|
|
749
853
|
finalCSS += cssFinal + "\n";
|
|
750
854
|
} catch (e) {
|
|
@@ -759,14 +863,18 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
759
863
|
const src = rule.style.getPropertyValue("src");
|
|
760
864
|
const family = rule.style.getPropertyValue("font-family");
|
|
761
865
|
if (!src || isIconFont(family)) continue;
|
|
866
|
+
const weightVal = rule.style.getPropertyValue("font-weight") || "normal";
|
|
867
|
+
const styleVal = rule.style.getPropertyValue("font-style") || "normal";
|
|
868
|
+
const key = `${family}__${weightVal}__${styleVal}`;
|
|
762
869
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
763
870
|
const localRegex = /local\((["']?)[^)]+?\1\)/g;
|
|
764
871
|
const hasURL = !!src.match(urlRegex);
|
|
765
872
|
const hasLocal = !!src.match(localRegex);
|
|
766
873
|
if (!hasURL && hasLocal) {
|
|
767
|
-
finalCSS += `@font-face{font-family:${family};src:${src};font-style:${
|
|
874
|
+
finalCSS += `@font-face{font-family:${family};src:${src};font-style:${styleVal};font-weight:${weightVal};}`;
|
|
768
875
|
continue;
|
|
769
876
|
}
|
|
877
|
+
if (!loadedFonts.has(key)) continue;
|
|
770
878
|
let inlinedSrc = src;
|
|
771
879
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
772
880
|
for (const match of matches) {
|
|
@@ -784,7 +892,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
784
892
|
}
|
|
785
893
|
if (cache.font.has(url)) continue;
|
|
786
894
|
try {
|
|
787
|
-
const res = await
|
|
895
|
+
const res = await fetchResource(url, { useProxy });
|
|
788
896
|
const blob = await res.blob();
|
|
789
897
|
const b64 = await new Promise((resolve) => {
|
|
790
898
|
const reader = new FileReader();
|
|
@@ -798,7 +906,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
798
906
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
799
907
|
}
|
|
800
908
|
}
|
|
801
|
-
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${
|
|
909
|
+
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleVal};font-weight:${weightVal};}`;
|
|
802
910
|
}
|
|
803
911
|
}
|
|
804
912
|
}
|
|
@@ -816,7 +924,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
816
924
|
cache.font.add(font._snapdomSrc);
|
|
817
925
|
} else if (!cache.font.has(font._snapdomSrc)) {
|
|
818
926
|
try {
|
|
819
|
-
const res = await
|
|
927
|
+
const res = await fetchResource(font._snapdomSrc, { useProxy });
|
|
820
928
|
const blob = await res.blob();
|
|
821
929
|
b64 = await new Promise((resolve) => {
|
|
822
930
|
const reader = new FileReader();
|
|
@@ -834,6 +942,32 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
834
942
|
finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
|
|
835
943
|
}
|
|
836
944
|
}
|
|
945
|
+
for (const font of localFonts) {
|
|
946
|
+
if (!font || typeof font !== "object") continue;
|
|
947
|
+
const { family, src, weight = "normal", style = "normal" } = font;
|
|
948
|
+
if (!family || !src) continue;
|
|
949
|
+
let b64 = src;
|
|
950
|
+
if (!b64.startsWith("data:")) {
|
|
951
|
+
try {
|
|
952
|
+
const res = await fetchResource(src, { useProxy });
|
|
953
|
+
const blob = await res.blob();
|
|
954
|
+
b64 = await new Promise((resolve) => {
|
|
955
|
+
const reader = new FileReader();
|
|
956
|
+
reader.onload = () => resolve(reader.result);
|
|
957
|
+
reader.readAsDataURL(blob);
|
|
958
|
+
});
|
|
959
|
+
cache.resource.set(src, b64);
|
|
960
|
+
cache.font.add(src);
|
|
961
|
+
} catch (e) {
|
|
962
|
+
console.warn("[snapdom] Failed to load local font:", src);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
cache.resource.set(src, b64);
|
|
967
|
+
cache.font.add(src);
|
|
968
|
+
}
|
|
969
|
+
finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};}`;
|
|
970
|
+
}
|
|
837
971
|
if (finalCSS) {
|
|
838
972
|
cache.resource.set("fonts-embed-css", finalCSS);
|
|
839
973
|
if (preCached) {
|
|
@@ -847,7 +981,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
847
981
|
}
|
|
848
982
|
|
|
849
983
|
// src/modules/pseudo.js
|
|
850
|
-
async function inlinePseudoElements(source, clone, options) {
|
|
984
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, options) {
|
|
851
985
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
852
986
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
853
987
|
try {
|
|
@@ -873,7 +1007,7 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
873
1007
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
874
1008
|
const snapshot2 = snapshotComputedStyle(style);
|
|
875
1009
|
const key2 = getStyleKey(snapshot2, "span", options);
|
|
876
|
-
|
|
1010
|
+
styleMap.set(span, key2);
|
|
877
1011
|
const restNode = document.createTextNode(rest);
|
|
878
1012
|
clone.replaceChild(restNode, textNode);
|
|
879
1013
|
clone.insertBefore(span, restNode);
|
|
@@ -907,7 +1041,7 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
907
1041
|
pseudoEl.style.verticalAlign = "middle";
|
|
908
1042
|
const snapshot = snapshotComputedStyle(style);
|
|
909
1043
|
const key = getStyleKey(snapshot, "span", options);
|
|
910
|
-
|
|
1044
|
+
styleMap.set(pseudoEl, key);
|
|
911
1045
|
if (isIconFont2 && cleanContent.length === 1) {
|
|
912
1046
|
const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
913
1047
|
const imgEl = document.createElement("img");
|
|
@@ -956,7 +1090,7 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
956
1090
|
const sChildren = Array.from(source.children);
|
|
957
1091
|
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
958
1092
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
959
|
-
await inlinePseudoElements(sChildren[i], cChildren[i], options);
|
|
1093
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, options);
|
|
960
1094
|
}
|
|
961
1095
|
}
|
|
962
1096
|
|
|
@@ -1012,6 +1146,9 @@ function inlineExternalDefsAndSymbols(rootElement) {
|
|
|
1012
1146
|
|
|
1013
1147
|
// src/core/prepare.js
|
|
1014
1148
|
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
1149
|
+
const styleMap = /* @__PURE__ */ new Map();
|
|
1150
|
+
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
1151
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1015
1152
|
let clone;
|
|
1016
1153
|
let classCSS = "";
|
|
1017
1154
|
stabilizeLayout(element);
|
|
@@ -1021,20 +1158,21 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1021
1158
|
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1022
1159
|
}
|
|
1023
1160
|
try {
|
|
1024
|
-
clone = deepClone(element, compress, options, element);
|
|
1161
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
|
|
1025
1162
|
} catch (e) {
|
|
1026
1163
|
console.warn("deepClone failed:", e);
|
|
1027
1164
|
throw e;
|
|
1028
1165
|
}
|
|
1029
1166
|
try {
|
|
1030
|
-
await inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
|
|
1167
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
|
|
1031
1168
|
} catch (e) {
|
|
1032
1169
|
console.warn("inlinePseudoElements failed:", e);
|
|
1033
1170
|
}
|
|
1171
|
+
await resolveBlobUrlsInTree(clone);
|
|
1034
1172
|
if (compress) {
|
|
1035
|
-
const keyToClass = generateCSSClasses();
|
|
1173
|
+
const keyToClass = generateCSSClasses(styleMap);
|
|
1036
1174
|
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
1037
|
-
for (const [node, key] of
|
|
1175
|
+
for (const [node, key] of styleMap.entries()) {
|
|
1038
1176
|
if (node.tagName === "STYLE") continue;
|
|
1039
1177
|
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
1040
1178
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
@@ -1051,12 +1189,12 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1051
1189
|
}
|
|
1052
1190
|
}
|
|
1053
1191
|
} else {
|
|
1054
|
-
for (const [node, key] of
|
|
1192
|
+
for (const [node, key] of styleMap.entries()) {
|
|
1055
1193
|
if (node.tagName === "STYLE") continue;
|
|
1056
1194
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1057
1195
|
}
|
|
1058
1196
|
}
|
|
1059
|
-
for (const [cloneNode, originalNode] of
|
|
1197
|
+
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1060
1198
|
const scrollX = originalNode.scrollLeft;
|
|
1061
1199
|
const scrollY = originalNode.scrollTop;
|
|
1062
1200
|
const hasScroll = scrollX || scrollY;
|
|
@@ -1075,9 +1213,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1075
1213
|
cloneNode.appendChild(inner);
|
|
1076
1214
|
}
|
|
1077
1215
|
}
|
|
1078
|
-
if (element ===
|
|
1079
|
-
const computed =
|
|
1080
|
-
|
|
1216
|
+
if (element === nodeMap.get(clone)) {
|
|
1217
|
+
const computed = styleCache.get(element) || window.getComputedStyle(element);
|
|
1218
|
+
styleCache.set(element, computed);
|
|
1081
1219
|
const transform = stripTranslate(computed.transform);
|
|
1082
1220
|
clone.style.margin = "0";
|
|
1083
1221
|
clone.style.position = "static";
|
|
@@ -1090,13 +1228,13 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1090
1228
|
clone.style.clear = "none";
|
|
1091
1229
|
clone.style.transform = transform || "";
|
|
1092
1230
|
}
|
|
1093
|
-
for (const [cloneNode, originalNode] of
|
|
1231
|
+
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1094
1232
|
if (originalNode.tagName === "PRE") {
|
|
1095
1233
|
cloneNode.style.marginTop = "0";
|
|
1096
1234
|
cloneNode.style.marginBlockStart = "0";
|
|
1097
1235
|
}
|
|
1098
1236
|
}
|
|
1099
|
-
return { clone, classCSS };
|
|
1237
|
+
return { clone, classCSS, styleCache };
|
|
1100
1238
|
}
|
|
1101
1239
|
function stabilizeLayout(element) {
|
|
1102
1240
|
const style = getComputedStyle(element);
|
|
@@ -1110,11 +1248,140 @@ function stabilizeLayout(element) {
|
|
|
1110
1248
|
element.style.border = `${outlineWidth} solid transparent`;
|
|
1111
1249
|
}
|
|
1112
1250
|
}
|
|
1251
|
+
var _blobToDataUrlCache = /* @__PURE__ */ new Map();
|
|
1252
|
+
async function blobUrlToDataUrl(blobUrl) {
|
|
1253
|
+
if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
|
|
1254
|
+
const res = await fetch(blobUrl);
|
|
1255
|
+
if (!res.ok) throw new Error(`[SnapDOM] HTTP ${res.status} on blob fetch (${blobUrl})`);
|
|
1256
|
+
const blob = await res.blob();
|
|
1257
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
1258
|
+
const fr = new FileReader();
|
|
1259
|
+
fr.onloadend = () => {
|
|
1260
|
+
const v = fr.result;
|
|
1261
|
+
if (typeof v === "string" && v.startsWith("data:")) resolve(v);
|
|
1262
|
+
else reject(new Error("[SnapDOM] Invalid data URL from blob"));
|
|
1263
|
+
};
|
|
1264
|
+
fr.onerror = () => reject(new Error("[SnapDOM] FileReader error"));
|
|
1265
|
+
fr.readAsDataURL(blob);
|
|
1266
|
+
});
|
|
1267
|
+
_blobToDataUrlCache.set(blobUrl, dataUrl);
|
|
1268
|
+
return dataUrl;
|
|
1269
|
+
}
|
|
1270
|
+
var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
|
|
1271
|
+
async function replaceBlobUrlsInCssText(cssText) {
|
|
1272
|
+
if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
|
|
1273
|
+
const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
|
|
1274
|
+
if (uniques.length === 0) return cssText;
|
|
1275
|
+
let out = cssText;
|
|
1276
|
+
for (const u of uniques) {
|
|
1277
|
+
try {
|
|
1278
|
+
const d = await blobUrlToDataUrl(u);
|
|
1279
|
+
out = out.split(u).join(d);
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return out;
|
|
1284
|
+
}
|
|
1285
|
+
function isBlobUrl(u) {
|
|
1286
|
+
return typeof u === "string" && u.startsWith("blob:");
|
|
1287
|
+
}
|
|
1288
|
+
function parseSrcset(srcset) {
|
|
1289
|
+
return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
|
|
1290
|
+
const m = item.match(/^(\S+)(\s+.+)?$/);
|
|
1291
|
+
return m ? { url: m[1], desc: m[2] || "" } : null;
|
|
1292
|
+
}).filter(Boolean);
|
|
1293
|
+
}
|
|
1294
|
+
function stringifySrcset(parts) {
|
|
1295
|
+
return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
|
|
1296
|
+
}
|
|
1297
|
+
async function resolveBlobUrlsInTree(root) {
|
|
1298
|
+
if (!root) return;
|
|
1299
|
+
const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
|
|
1300
|
+
for (const img of imgs) {
|
|
1301
|
+
try {
|
|
1302
|
+
const srcAttr = img.getAttribute("src");
|
|
1303
|
+
const effective = srcAttr || img.currentSrc || "";
|
|
1304
|
+
if (isBlobUrl(effective)) {
|
|
1305
|
+
const data = await blobUrlToDataUrl(effective);
|
|
1306
|
+
img.setAttribute("src", data);
|
|
1307
|
+
}
|
|
1308
|
+
const srcset = img.getAttribute("srcset");
|
|
1309
|
+
if (srcset && srcset.includes("blob:")) {
|
|
1310
|
+
const parts = parseSrcset(srcset);
|
|
1311
|
+
let changed = false;
|
|
1312
|
+
for (const p of parts) {
|
|
1313
|
+
if (isBlobUrl(p.url)) {
|
|
1314
|
+
try {
|
|
1315
|
+
p.url = await blobUrlToDataUrl(p.url);
|
|
1316
|
+
changed = true;
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (changed) img.setAttribute("srcset", stringifySrcset(parts));
|
|
1322
|
+
}
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
|
|
1327
|
+
for (const node of svgImages) {
|
|
1328
|
+
try {
|
|
1329
|
+
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1330
|
+
const href = node.getAttribute("href") || node.getAttributeNS?.(XLINK_NS, "href");
|
|
1331
|
+
if (isBlobUrl(href)) {
|
|
1332
|
+
const d = await blobUrlToDataUrl(href);
|
|
1333
|
+
node.setAttribute("href", d);
|
|
1334
|
+
node.removeAttributeNS?.(XLINK_NS, "href");
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
|
|
1340
|
+
for (const el of styled) {
|
|
1341
|
+
try {
|
|
1342
|
+
const styleText = el.getAttribute("style");
|
|
1343
|
+
if (styleText && styleText.includes("blob:")) {
|
|
1344
|
+
const replaced = await replaceBlobUrlsInCssText(styleText);
|
|
1345
|
+
el.setAttribute("style", replaced);
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
|
|
1351
|
+
for (const s of styleTags) {
|
|
1352
|
+
try {
|
|
1353
|
+
const css = s.textContent || "";
|
|
1354
|
+
if (css.includes("blob:")) {
|
|
1355
|
+
s.textContent = await replaceBlobUrlsInCssText(css);
|
|
1356
|
+
}
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const urlAttrs = ["poster"];
|
|
1361
|
+
for (const attr of urlAttrs) {
|
|
1362
|
+
const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
|
|
1363
|
+
for (const n of nodes) {
|
|
1364
|
+
try {
|
|
1365
|
+
const u = n.getAttribute(attr);
|
|
1366
|
+
if (isBlobUrl(u)) {
|
|
1367
|
+
n.setAttribute(attr, await blobUrlToDataUrl(u));
|
|
1368
|
+
}
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1113
1374
|
|
|
1114
1375
|
// src/modules/images.js
|
|
1115
1376
|
async function inlineImages(clone, options = {}) {
|
|
1116
1377
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
1117
1378
|
const processImg = async (img) => {
|
|
1379
|
+
if (!img.getAttribute("src")) {
|
|
1380
|
+
const eff = img.currentSrc || img.src || "";
|
|
1381
|
+
if (eff) img.setAttribute("src", eff);
|
|
1382
|
+
}
|
|
1383
|
+
img.removeAttribute("srcset");
|
|
1384
|
+
img.removeAttribute("sizes");
|
|
1118
1385
|
const src = img.src;
|
|
1119
1386
|
try {
|
|
1120
1387
|
const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
|
|
@@ -1135,7 +1402,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
1135
1402
|
}
|
|
1136
1403
|
|
|
1137
1404
|
// src/modules/background.js
|
|
1138
|
-
async function inlineBackgroundImages(source, clone, options = {}) {
|
|
1405
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
1139
1406
|
const queue = [[source, clone]];
|
|
1140
1407
|
const imageProps = [
|
|
1141
1408
|
"background-image",
|
|
@@ -1155,8 +1422,8 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1155
1422
|
];
|
|
1156
1423
|
while (queue.length) {
|
|
1157
1424
|
const [srcNode, cloneNode] = queue.shift();
|
|
1158
|
-
const style =
|
|
1159
|
-
if (!
|
|
1425
|
+
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
1426
|
+
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
1160
1427
|
const hasBorderImage = (() => {
|
|
1161
1428
|
const bi = style.getPropertyValue("border-image");
|
|
1162
1429
|
const bis = style.getPropertyValue("border-image-source");
|
|
@@ -1192,13 +1459,13 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1192
1459
|
async function captureDOM(element, options = {}) {
|
|
1193
1460
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1194
1461
|
cache.reset();
|
|
1195
|
-
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
1196
|
-
let clone, classCSS;
|
|
1462
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "", localFonts = [] } = options;
|
|
1463
|
+
let clone, classCSS, styleCache;
|
|
1197
1464
|
let fontsCSS = "";
|
|
1198
1465
|
let baseCSS = "";
|
|
1199
1466
|
let dataURL;
|
|
1200
1467
|
let svgString;
|
|
1201
|
-
({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));
|
|
1468
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
|
|
1202
1469
|
await new Promise((resolve) => {
|
|
1203
1470
|
idle(async () => {
|
|
1204
1471
|
await inlineImages(clone, options);
|
|
@@ -1207,14 +1474,14 @@ async function captureDOM(element, options = {}) {
|
|
|
1207
1474
|
});
|
|
1208
1475
|
await new Promise((resolve) => {
|
|
1209
1476
|
idle(async () => {
|
|
1210
|
-
await inlineBackgroundImages(element, clone, options);
|
|
1477
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
1211
1478
|
resolve();
|
|
1212
1479
|
}, { fast });
|
|
1213
1480
|
});
|
|
1214
1481
|
if (embedFonts) {
|
|
1215
1482
|
await new Promise((resolve) => {
|
|
1216
1483
|
idle(async () => {
|
|
1217
|
-
fontsCSS = await embedCustomFonts();
|
|
1484
|
+
fontsCSS = await embedCustomFonts({ localFonts, useProxy });
|
|
1218
1485
|
resolve();
|
|
1219
1486
|
}, { fast });
|
|
1220
1487
|
});
|
|
@@ -1449,12 +1716,21 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1449
1716
|
|
|
1450
1717
|
// src/api/preCache.js
|
|
1451
1718
|
async function preCache(root = document, options = {}) {
|
|
1452
|
-
const { embedFonts = true, reset = false } = options;
|
|
1719
|
+
const { embedFonts = true, reset = false, useProxy } = options;
|
|
1453
1720
|
if (reset) {
|
|
1454
|
-
cache.
|
|
1721
|
+
cache.image.clear();
|
|
1722
|
+
cache.background.clear();
|
|
1723
|
+
cache.resource.clear();
|
|
1724
|
+
cache.defaultStyle.clear();
|
|
1725
|
+
cache.baseStyle.clear();
|
|
1726
|
+
cache.font.clear();
|
|
1727
|
+
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
1455
1728
|
return;
|
|
1456
1729
|
}
|
|
1457
|
-
|
|
1730
|
+
try {
|
|
1731
|
+
await document.fonts.ready;
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1458
1734
|
precacheCommonTags();
|
|
1459
1735
|
let imgEls = [], allEls = [];
|
|
1460
1736
|
if (root?.querySelectorAll) {
|
|
@@ -1463,33 +1739,41 @@ async function preCache(root = document, options = {}) {
|
|
|
1463
1739
|
}
|
|
1464
1740
|
const promises = [];
|
|
1465
1741
|
for (const img of imgEls) {
|
|
1466
|
-
const src = img
|
|
1742
|
+
const src = img?.src;
|
|
1743
|
+
if (!src) continue;
|
|
1467
1744
|
if (!cache.image.has(src)) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
);
|
|
1745
|
+
const p = Promise.resolve().then(() => fetchImage(src, { useProxy })).then((dataURL) => {
|
|
1746
|
+
cache.image.set(src, dataURL);
|
|
1747
|
+
}).catch(() => {
|
|
1748
|
+
});
|
|
1749
|
+
promises.push(p);
|
|
1472
1750
|
}
|
|
1473
1751
|
}
|
|
1474
1752
|
for (const el of allEls) {
|
|
1475
|
-
|
|
1753
|
+
let bg = "";
|
|
1754
|
+
try {
|
|
1755
|
+
bg = getStyle(el).backgroundImage;
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1476
1758
|
if (bg && bg !== "none") {
|
|
1477
1759
|
const bgSplits = splitBackgroundImage(bg);
|
|
1478
1760
|
for (const entry of bgSplits) {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
})
|
|
1484
|
-
);
|
|
1761
|
+
if (entry.startsWith("url(")) {
|
|
1762
|
+
const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
|
|
1763
|
+
});
|
|
1764
|
+
promises.push(p);
|
|
1485
1765
|
}
|
|
1486
1766
|
}
|
|
1487
1767
|
}
|
|
1488
1768
|
}
|
|
1489
1769
|
if (embedFonts) {
|
|
1490
|
-
|
|
1770
|
+
try {
|
|
1771
|
+
await embedCustomFonts({ preCached: true, localFonts: options.localFonts, useProxy: options.useProxy });
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
1774
|
+
;
|
|
1491
1775
|
}
|
|
1492
|
-
await Promise.
|
|
1776
|
+
await Promise.allSettled(promises);
|
|
1493
1777
|
}
|
|
1494
1778
|
export {
|
|
1495
1779
|
preCache,
|