@zumer/snapdom 1.9.7 → 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 +58 -8
- package/dist/snapdom.js +660 -281
- package/dist/snapdom.min.js +3 -3
- package/dist/snapdom.min.mjs +3 -3
- package/dist/snapdom.mjs +636 -277
- package/package.json +1 -1
- package/types/snapdom.d.ts +4 -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
|
|
@@ -81,7 +73,7 @@ function getDefaultStyleForTag(tagName) {
|
|
|
81
73
|
if (cache.defaultStyle.has(tagName)) {
|
|
82
74
|
return cache.defaultStyle.get(tagName);
|
|
83
75
|
}
|
|
84
|
-
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
|
|
76
|
+
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
|
|
85
77
|
if (skipTags.has(tagName)) {
|
|
86
78
|
const empty = {};
|
|
87
79
|
cache.defaultStyle.set(tagName, empty);
|
|
@@ -111,10 +103,14 @@ function getDefaultStyleForTag(tagName) {
|
|
|
111
103
|
cache.defaultStyle.set(tagName, defaults);
|
|
112
104
|
return defaults;
|
|
113
105
|
}
|
|
106
|
+
var IGNORED_PROPS = /* @__PURE__ */ new Set([
|
|
107
|
+
"-webkit-locale"
|
|
108
|
+
]);
|
|
114
109
|
function getStyleKey(snapshot, tagName, compress = false) {
|
|
115
110
|
const entries = [];
|
|
116
111
|
const defaultStyles = getDefaultStyleForTag(tagName);
|
|
117
112
|
for (let [prop, value] of Object.entries(snapshot)) {
|
|
113
|
+
if (IGNORED_PROPS.has(prop)) continue;
|
|
118
114
|
if (!compress) {
|
|
119
115
|
if (value) {
|
|
120
116
|
entries.push(`${prop}:${value}`);
|
|
@@ -159,11 +155,12 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
159
155
|
}
|
|
160
156
|
return css;
|
|
161
157
|
}
|
|
162
|
-
function generateCSSClasses() {
|
|
163
|
-
const keySet = new Set(
|
|
158
|
+
function generateCSSClasses(styleMap) {
|
|
159
|
+
const keySet = new Set(styleMap.values());
|
|
164
160
|
const classMap = /* @__PURE__ */ new Map();
|
|
165
161
|
let counter = 1;
|
|
166
162
|
for (const key of keySet) {
|
|
163
|
+
if (!key.trim()) continue;
|
|
167
164
|
classMap.set(key, `c${counter++}`);
|
|
168
165
|
}
|
|
169
166
|
return classMap;
|
|
@@ -229,7 +226,25 @@ function extractURL(value) {
|
|
|
229
226
|
if (url.startsWith("#")) return null;
|
|
230
227
|
return url;
|
|
231
228
|
}
|
|
232
|
-
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 } = {}) {
|
|
233
248
|
function getCrossOriginMode(url) {
|
|
234
249
|
try {
|
|
235
250
|
const parsed = new URL(url, window.location.href);
|
|
@@ -238,105 +253,157 @@ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
|
238
253
|
return "anonymous";
|
|
239
254
|
}
|
|
240
255
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
245
|
-
}).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
|
|
246
|
-
const reader = new FileReader();
|
|
247
|
-
reader.onloadend = () => {
|
|
248
|
-
const base64 = reader.result;
|
|
249
|
-
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
250
|
-
reject(new Error("Invalid image data URL"));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
resolve(base64);
|
|
254
|
-
};
|
|
255
|
-
reader.onerror = () => reject(new Error("FileReader error"));
|
|
256
|
-
reader.readAsDataURL(blob);
|
|
257
|
-
}));
|
|
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) {
|
|
258
259
|
try {
|
|
259
|
-
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));
|
|
260
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;
|
|
261
286
|
if (useProxy && typeof useProxy === "string") {
|
|
262
287
|
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
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
|
+
});
|
|
270
292
|
}
|
|
271
|
-
|
|
293
|
+
return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
|
|
294
|
+
});
|
|
272
295
|
}
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
303
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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/")) {
|
|
279
308
|
cache.image.set(src, src);
|
|
280
309
|
return Promise.resolve(src);
|
|
281
310
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const response = await fetch(src, {
|
|
287
|
-
mode: "cors",
|
|
288
|
-
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
289
|
-
});
|
|
290
|
-
const svgText = await response.text();
|
|
291
|
-
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
292
|
-
cache.image.set(src, encoded);
|
|
293
|
-
return encoded;
|
|
294
|
-
} catch {
|
|
295
|
-
return fetchWithFallback(src);
|
|
296
|
-
}
|
|
297
|
-
})();
|
|
298
|
-
}
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
300
|
-
const timeoutId = setTimeout(() => {
|
|
301
|
-
reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
|
|
302
|
-
}, timeout);
|
|
303
|
-
const image = new Image();
|
|
304
|
-
image.crossOrigin = crossOriginValue;
|
|
305
|
-
image.onload = async () => {
|
|
306
|
-
clearTimeout(timeoutId);
|
|
307
|
-
try {
|
|
308
|
-
await image.decode();
|
|
309
|
-
const canvas = document.createElement("canvas");
|
|
310
|
-
canvas.width = image.width;
|
|
311
|
-
canvas.height = image.height;
|
|
312
|
-
const ctx = canvas.getContext("2d");
|
|
313
|
-
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
314
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
315
|
-
cache.image.set(src, dataURL);
|
|
316
|
-
resolve(dataURL);
|
|
317
|
-
} catch {
|
|
311
|
+
if (/\.svg(\?.*)?$/i.test(src)) {
|
|
312
|
+
const p2 = (async () => {
|
|
313
|
+
const direct = await (async () => {
|
|
318
314
|
try {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
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)}`);
|
|
322
322
|
} catch (e) {
|
|
323
|
-
|
|
323
|
+
return fail(e);
|
|
324
324
|
}
|
|
325
|
+
})();
|
|
326
|
+
if (direct.ok) {
|
|
327
|
+
cache.image.set(src, direct.data);
|
|
328
|
+
return direct.data;
|
|
325
329
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
const fallbackDataURL = await fetchWithFallback(src);
|
|
332
|
-
cache.image.set(src, fallbackDataURL);
|
|
333
|
-
resolve(fallbackDataURL);
|
|
334
|
-
} catch (e) {
|
|
335
|
-
reject(e);
|
|
330
|
+
const via = await fetchWithFallbackOnceSafe(src);
|
|
331
|
+
if (via.ok) {
|
|
332
|
+
cache.image.set(src, via.data);
|
|
333
|
+
return via.data;
|
|
336
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);
|
|
337
353
|
};
|
|
338
|
-
|
|
354
|
+
const onSuccess = (d) => {
|
|
355
|
+
cache.image.set(src, d);
|
|
356
|
+
resolve(d);
|
|
357
|
+
};
|
|
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;
|
|
401
|
+
});
|
|
402
|
+
_inflight.set(src, p);
|
|
403
|
+
p.finally(() => _inflight.delete(src));
|
|
404
|
+
p.catch(() => {
|
|
339
405
|
});
|
|
406
|
+
return p;
|
|
340
407
|
}
|
|
341
408
|
function snapshotComputedStyle(style) {
|
|
342
409
|
const snap = {};
|
|
@@ -393,8 +460,11 @@ function splitBackgroundImage(bg) {
|
|
|
393
460
|
}
|
|
394
461
|
|
|
395
462
|
// src/modules/styles.js
|
|
463
|
+
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
464
|
+
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
396
465
|
function snapshotComputedStyleFull(style) {
|
|
397
466
|
const result = {};
|
|
467
|
+
const computedVisibility = style.getPropertyValue("visibility");
|
|
398
468
|
for (let i = 0; i < style.length; i++) {
|
|
399
469
|
const prop = style[i];
|
|
400
470
|
let val = style.getPropertyValue(prop);
|
|
@@ -403,32 +473,47 @@ function snapshotComputedStyleFull(style) {
|
|
|
403
473
|
}
|
|
404
474
|
result[prop] = val;
|
|
405
475
|
}
|
|
476
|
+
if (computedVisibility === "hidden") {
|
|
477
|
+
result.opacity = "0";
|
|
478
|
+
}
|
|
406
479
|
return result;
|
|
407
480
|
}
|
|
408
|
-
function inlineAllStyles(source, clone, compress) {
|
|
481
|
+
function inlineAllStyles(source, clone, styleMap, cache2, compress) {
|
|
409
482
|
if (source.tagName === "STYLE") return;
|
|
410
|
-
if (!
|
|
411
|
-
|
|
483
|
+
if (!cache2.has(source)) {
|
|
484
|
+
cache2.set(source, getStyle(source));
|
|
412
485
|
}
|
|
413
|
-
const style =
|
|
414
|
-
if (!
|
|
486
|
+
const style = cache2.get(source);
|
|
487
|
+
if (!snapshotCache.has(source)) {
|
|
415
488
|
const snapshot2 = snapshotComputedStyleFull(style);
|
|
416
|
-
|
|
489
|
+
snapshotCache.set(source, snapshot2);
|
|
417
490
|
}
|
|
418
|
-
const snapshot =
|
|
491
|
+
const snapshot = snapshotCache.get(source);
|
|
419
492
|
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
420
|
-
if (
|
|
421
|
-
|
|
493
|
+
if (snapshotKeyCache.has(hash)) {
|
|
494
|
+
styleMap.set(clone, snapshotKeyCache.get(hash));
|
|
422
495
|
return;
|
|
423
496
|
}
|
|
424
497
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
425
498
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
426
|
-
|
|
427
|
-
|
|
499
|
+
snapshotKeyCache.set(hash, key);
|
|
500
|
+
styleMap.set(clone, key);
|
|
428
501
|
}
|
|
429
502
|
|
|
430
503
|
// src/core/clone.js
|
|
431
|
-
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) {
|
|
432
517
|
if (!node) throw new Error("Invalid node");
|
|
433
518
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
434
519
|
let pendingSelectValue = null;
|
|
@@ -477,8 +562,8 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
477
562
|
}
|
|
478
563
|
if (node.getAttribute("data-capture") === "placeholder") {
|
|
479
564
|
const clone2 = node.cloneNode(false);
|
|
480
|
-
|
|
481
|
-
inlineAllStyles(node, clone2, compress);
|
|
565
|
+
nodeMap.set(clone2, node);
|
|
566
|
+
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
482
567
|
const placeholder = document.createElement("div");
|
|
483
568
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
484
569
|
placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
|
|
@@ -491,14 +576,17 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
491
576
|
img.src = dataURL;
|
|
492
577
|
img.width = node.width;
|
|
493
578
|
img.height = node.height;
|
|
494
|
-
|
|
495
|
-
inlineAllStyles(node, img, compress);
|
|
579
|
+
nodeMap.set(img, node);
|
|
580
|
+
inlineAllStyles(node, img, styleMap, styleCache, compress);
|
|
496
581
|
return img;
|
|
497
582
|
}
|
|
498
583
|
let clone;
|
|
499
584
|
try {
|
|
500
585
|
clone = node.cloneNode(false);
|
|
501
|
-
|
|
586
|
+
nodeMap.set(clone, node);
|
|
587
|
+
if (node.tagName === "IMG") {
|
|
588
|
+
freezeImgSrcset(node, clone);
|
|
589
|
+
}
|
|
502
590
|
} catch (err) {
|
|
503
591
|
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
504
592
|
throw err;
|
|
@@ -523,7 +611,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
523
611
|
if (node instanceof HTMLSelectElement) {
|
|
524
612
|
pendingSelectValue = node.value;
|
|
525
613
|
}
|
|
526
|
-
inlineAllStyles(node, clone, compress);
|
|
614
|
+
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
527
615
|
if (node.shadowRoot) {
|
|
528
616
|
const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
|
|
529
617
|
if (hasSlot) {
|
|
@@ -531,8 +619,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
531
619
|
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
532
620
|
const cssText = child.textContent || "";
|
|
533
621
|
if (cssText.trim() && compress) {
|
|
534
|
-
|
|
535
|
-
cache.preStyle.set(child, cssText);
|
|
622
|
+
styleCache.set(child, cssText);
|
|
536
623
|
}
|
|
537
624
|
}
|
|
538
625
|
}
|
|
@@ -542,12 +629,11 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
542
629
|
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
543
630
|
const cssText = child.textContent || "";
|
|
544
631
|
if (cssText.trim() && compress) {
|
|
545
|
-
|
|
546
|
-
cache.preStyle.set(child, cssText);
|
|
632
|
+
styleCache.set(child, cssText);
|
|
547
633
|
}
|
|
548
634
|
continue;
|
|
549
635
|
}
|
|
550
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
636
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
551
637
|
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
552
638
|
}
|
|
553
639
|
clone.appendChild(shadowFrag);
|
|
@@ -558,14 +644,14 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
558
644
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
559
645
|
const fragment = document.createDocumentFragment();
|
|
560
646
|
for (const child of nodesToClone) {
|
|
561
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
647
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
562
648
|
if (clonedChild) fragment.appendChild(clonedChild);
|
|
563
649
|
}
|
|
564
650
|
return fragment;
|
|
565
651
|
}
|
|
566
652
|
for (const child of node.childNodes) {
|
|
567
653
|
if (clonedAssignedNodes.has(child)) continue;
|
|
568
|
-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
|
|
654
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
569
655
|
if (clonedChild) clone.appendChild(clonedChild);
|
|
570
656
|
}
|
|
571
657
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
@@ -622,25 +708,38 @@ function isIconFont(input) {
|
|
|
622
708
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
623
709
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
624
710
|
const dpr = window.devicePixelRatio || 1;
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
711
|
+
await document.fonts.ready;
|
|
712
|
+
const span = document.createElement("span");
|
|
713
|
+
span.textContent = unicodeChar;
|
|
714
|
+
span.style.position = "absolute";
|
|
715
|
+
span.style.visibility = "hidden";
|
|
716
|
+
span.style.fontFamily = `"${fontFamily}"`;
|
|
717
|
+
span.style.fontWeight = fontWeight || "normal";
|
|
718
|
+
span.style.fontSize = `${fontSize}px`;
|
|
719
|
+
span.style.lineHeight = "1";
|
|
720
|
+
span.style.whiteSpace = "nowrap";
|
|
721
|
+
span.style.padding = "0";
|
|
722
|
+
span.style.margin = "0";
|
|
723
|
+
document.body.appendChild(span);
|
|
724
|
+
const rect = span.getBoundingClientRect();
|
|
725
|
+
const width = Math.ceil(rect.width);
|
|
726
|
+
const height = Math.ceil(rect.height);
|
|
727
|
+
document.body.removeChild(span);
|
|
633
728
|
const canvas = document.createElement("canvas");
|
|
634
|
-
canvas.width =
|
|
635
|
-
canvas.height =
|
|
729
|
+
canvas.width = width * dpr;
|
|
730
|
+
canvas.height = height * dpr;
|
|
636
731
|
const ctx = canvas.getContext("2d");
|
|
637
732
|
ctx.scale(dpr, dpr);
|
|
638
|
-
ctx.font =
|
|
733
|
+
ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
|
|
639
734
|
ctx.textAlign = "left";
|
|
640
|
-
ctx.textBaseline = "
|
|
735
|
+
ctx.textBaseline = "top";
|
|
641
736
|
ctx.fillStyle = color;
|
|
642
|
-
ctx.fillText(unicodeChar, 0,
|
|
643
|
-
return
|
|
737
|
+
ctx.fillText(unicodeChar, 0, 0);
|
|
738
|
+
return {
|
|
739
|
+
dataUrl: canvas.toDataURL(),
|
|
740
|
+
width,
|
|
741
|
+
height
|
|
742
|
+
};
|
|
644
743
|
}
|
|
645
744
|
function isStylesheetLoaded(href) {
|
|
646
745
|
return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
|
|
@@ -657,7 +756,7 @@ function injectLinkIfMissing(href) {
|
|
|
657
756
|
document.head.appendChild(link);
|
|
658
757
|
});
|
|
659
758
|
}
|
|
660
|
-
async function embedCustomFonts({ preCached = false } = {}) {
|
|
759
|
+
async function embedCustomFonts({ preCached = false, localFonts = [], useProxy = "" } = {}) {
|
|
661
760
|
if (cache.resource.has("fonts-embed-css")) {
|
|
662
761
|
if (preCached) {
|
|
663
762
|
const style = document.createElement("style");
|
|
@@ -667,6 +766,15 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
667
766
|
}
|
|
668
767
|
return cache.resource.get("fonts-embed-css");
|
|
669
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
|
+
}
|
|
670
778
|
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
671
779
|
const styleImports = [];
|
|
672
780
|
for (const styleTag of document.querySelectorAll("style")) {
|
|
@@ -685,26 +793,48 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
685
793
|
let finalCSS = "";
|
|
686
794
|
for (const link of links) {
|
|
687
795
|
try {
|
|
688
|
-
const res = await
|
|
796
|
+
const res = await fetchResource(link.href, { useProxy });
|
|
689
797
|
const cssText = await res.text();
|
|
690
798
|
if (isIconFont(link.href) || isIconFont(cssText)) continue;
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
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) {
|
|
694
823
|
let rawUrl = extractURL(match[0]);
|
|
695
|
-
if (!rawUrl)
|
|
824
|
+
if (!rawUrl) continue;
|
|
696
825
|
let url = rawUrl;
|
|
697
826
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
698
827
|
url = new URL(url, link.href).href;
|
|
699
828
|
}
|
|
700
|
-
if (isIconFont(url))
|
|
829
|
+
if (isIconFont(url)) continue;
|
|
701
830
|
if (cache.resource.has(url)) {
|
|
702
831
|
cache.font.add(url);
|
|
703
|
-
|
|
832
|
+
inlined = inlined.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
833
|
+
continue;
|
|
704
834
|
}
|
|
705
|
-
if (cache.font.has(url))
|
|
835
|
+
if (cache.font.has(url)) continue;
|
|
706
836
|
try {
|
|
707
|
-
const fontRes = await
|
|
837
|
+
const fontRes = await fetchResource(url, { useProxy });
|
|
708
838
|
const blob = await fontRes.blob();
|
|
709
839
|
const b64 = await new Promise((resolve) => {
|
|
710
840
|
const reader = new FileReader();
|
|
@@ -713,16 +843,12 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
713
843
|
});
|
|
714
844
|
cache.resource.set(url, b64);
|
|
715
845
|
cache.font.add(url);
|
|
716
|
-
|
|
846
|
+
inlined = inlined.replace(match[0], `url(${b64})`);
|
|
717
847
|
} catch (e) {
|
|
718
848
|
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
719
|
-
return null;
|
|
720
849
|
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
let cssFinal = cssText;
|
|
724
|
-
for (const r of inlinedCSS) {
|
|
725
|
-
if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
850
|
+
}
|
|
851
|
+
cssFinal = cssFinal.replace(face, inlined);
|
|
726
852
|
}
|
|
727
853
|
finalCSS += cssFinal + "\n";
|
|
728
854
|
} catch (e) {
|
|
@@ -737,14 +863,18 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
737
863
|
const src = rule.style.getPropertyValue("src");
|
|
738
864
|
const family = rule.style.getPropertyValue("font-family");
|
|
739
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}`;
|
|
740
869
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
741
870
|
const localRegex = /local\((["']?)[^)]+?\1\)/g;
|
|
742
871
|
const hasURL = !!src.match(urlRegex);
|
|
743
872
|
const hasLocal = !!src.match(localRegex);
|
|
744
873
|
if (!hasURL && hasLocal) {
|
|
745
|
-
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};}`;
|
|
746
875
|
continue;
|
|
747
876
|
}
|
|
877
|
+
if (!loadedFonts.has(key)) continue;
|
|
748
878
|
let inlinedSrc = src;
|
|
749
879
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
750
880
|
for (const match of matches) {
|
|
@@ -762,7 +892,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
762
892
|
}
|
|
763
893
|
if (cache.font.has(url)) continue;
|
|
764
894
|
try {
|
|
765
|
-
const res = await
|
|
895
|
+
const res = await fetchResource(url, { useProxy });
|
|
766
896
|
const blob = await res.blob();
|
|
767
897
|
const b64 = await new Promise((resolve) => {
|
|
768
898
|
const reader = new FileReader();
|
|
@@ -776,7 +906,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
776
906
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
777
907
|
}
|
|
778
908
|
}
|
|
779
|
-
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};}`;
|
|
780
910
|
}
|
|
781
911
|
}
|
|
782
912
|
}
|
|
@@ -794,7 +924,7 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
794
924
|
cache.font.add(font._snapdomSrc);
|
|
795
925
|
} else if (!cache.font.has(font._snapdomSrc)) {
|
|
796
926
|
try {
|
|
797
|
-
const res = await
|
|
927
|
+
const res = await fetchResource(font._snapdomSrc, { useProxy });
|
|
798
928
|
const blob = await res.blob();
|
|
799
929
|
b64 = await new Promise((resolve) => {
|
|
800
930
|
const reader = new FileReader();
|
|
@@ -812,6 +942,32 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
812
942
|
finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
|
|
813
943
|
}
|
|
814
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
|
+
}
|
|
815
971
|
if (finalCSS) {
|
|
816
972
|
cache.resource.set("fonts-embed-css", finalCSS);
|
|
817
973
|
if (preCached) {
|
|
@@ -825,18 +981,20 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
825
981
|
}
|
|
826
982
|
|
|
827
983
|
// src/modules/pseudo.js
|
|
828
|
-
async function inlinePseudoElements(source, clone,
|
|
984
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, options) {
|
|
829
985
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
830
986
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
831
987
|
try {
|
|
832
988
|
const style = getStyle(source, pseudo);
|
|
833
989
|
if (!style || typeof style[Symbol.iterator] !== "function") continue;
|
|
990
|
+
const isEmptyPseudo = style.content === "none" && style.backgroundImage === "none" && style.backgroundColor === "transparent" && (style.borderStyle === "none" || parseFloat(style.borderWidth) === 0) && (!style.transform || style.transform === "none") && style.display === "inline";
|
|
991
|
+
if (isEmptyPseudo) continue;
|
|
834
992
|
if (pseudo === "::first-letter") {
|
|
835
993
|
const normal = getComputedStyle(source);
|
|
836
994
|
const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
|
|
837
995
|
if (!isMeaningful) continue;
|
|
838
996
|
const textNode = Array.from(clone.childNodes).find(
|
|
839
|
-
(n) => n.nodeType === Node.TEXT_NODE && n.textContent
|
|
997
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim().length > 0
|
|
840
998
|
);
|
|
841
999
|
if (!textNode) continue;
|
|
842
1000
|
const text = textNode.textContent;
|
|
@@ -848,57 +1006,55 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
|
|
|
848
1006
|
span.textContent = first;
|
|
849
1007
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
850
1008
|
const snapshot2 = snapshotComputedStyle(style);
|
|
851
|
-
const key2 = getStyleKey(snapshot2, "span",
|
|
852
|
-
|
|
1009
|
+
const key2 = getStyleKey(snapshot2, "span", options);
|
|
1010
|
+
styleMap.set(span, key2);
|
|
853
1011
|
const restNode = document.createTextNode(rest);
|
|
854
1012
|
clone.replaceChild(restNode, textNode);
|
|
855
1013
|
clone.insertBefore(span, restNode);
|
|
856
1014
|
continue;
|
|
857
1015
|
}
|
|
858
|
-
const content = style.
|
|
859
|
-
const
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
const
|
|
1016
|
+
const content = style.content;
|
|
1017
|
+
const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
|
|
1018
|
+
const bg = style.backgroundImage;
|
|
1019
|
+
const bgColor = style.backgroundColor;
|
|
1020
|
+
const fontFamily = style.fontFamily;
|
|
1021
|
+
const fontSize = parseInt(style.fontSize) || 32;
|
|
1022
|
+
const fontWeight = parseInt(style.fontWeight) || false;
|
|
1023
|
+
const color = style.color || "#000";
|
|
1024
|
+
const display = style.display;
|
|
1025
|
+
const width = parseFloat(style.width);
|
|
1026
|
+
const height = parseFloat(style.height);
|
|
1027
|
+
const borderStyle = style.borderStyle;
|
|
1028
|
+
const borderWidth = parseFloat(style.borderWidth);
|
|
1029
|
+
const transform = style.transform;
|
|
870
1030
|
const isIconFont2 = isIconFont(fontFamily);
|
|
871
|
-
|
|
872
|
-
if (/counter\s*\(|counters\s*\(/.test(content)) {
|
|
873
|
-
cleanContent = "- ";
|
|
874
|
-
} else {
|
|
875
|
-
cleanContent = parseContent(content);
|
|
876
|
-
}
|
|
877
|
-
const hasContent = content !== "none";
|
|
878
|
-
const hasExplicitContent = hasContent && cleanContent !== "";
|
|
1031
|
+
const hasExplicitContent = content !== "none" && cleanContent !== "";
|
|
879
1032
|
const hasBg = bg && bg !== "none";
|
|
880
1033
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
881
1034
|
const hasBox = display !== "inline" && (width > 0 || height > 0);
|
|
882
|
-
const hasBorder = borderStyle && borderStyle !== "none";
|
|
1035
|
+
const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
|
|
883
1036
|
const hasTransform = transform && transform !== "none";
|
|
884
1037
|
const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
|
|
885
1038
|
if (!shouldRender) continue;
|
|
886
1039
|
const pseudoEl = document.createElement("span");
|
|
887
1040
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
1041
|
+
pseudoEl.style.verticalAlign = "middle";
|
|
888
1042
|
const snapshot = snapshotComputedStyle(style);
|
|
889
|
-
const key = getStyleKey(snapshot, "span",
|
|
890
|
-
|
|
1043
|
+
const key = getStyleKey(snapshot, "span", options);
|
|
1044
|
+
styleMap.set(pseudoEl, key);
|
|
891
1045
|
if (isIconFont2 && cleanContent.length === 1) {
|
|
1046
|
+
const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
892
1047
|
const imgEl = document.createElement("img");
|
|
893
|
-
imgEl.src =
|
|
894
|
-
imgEl.style = `width:${fontSize}px;
|
|
1048
|
+
imgEl.src = dataUrl;
|
|
1049
|
+
imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
|
|
895
1050
|
pseudoEl.appendChild(imgEl);
|
|
1051
|
+
clone.dataset.snapdomHasIcon = "true";
|
|
896
1052
|
} else if (cleanContent.startsWith("url(")) {
|
|
897
1053
|
const rawUrl = extractURL(cleanContent);
|
|
898
|
-
if (rawUrl
|
|
1054
|
+
if (rawUrl?.trim()) {
|
|
899
1055
|
try {
|
|
900
1056
|
const imgEl = document.createElement("img");
|
|
901
|
-
const dataUrl = await fetchImage(safeEncodeURI(rawUrl,
|
|
1057
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl), options);
|
|
902
1058
|
imgEl.src = dataUrl;
|
|
903
1059
|
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
904
1060
|
pseudoEl.appendChild(imgEl);
|
|
@@ -912,19 +1068,21 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
|
|
|
912
1068
|
if (hasBg) {
|
|
913
1069
|
try {
|
|
914
1070
|
const bgSplits = splitBackgroundImage(bg);
|
|
915
|
-
const newBgParts = await Promise.all(
|
|
916
|
-
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
|
|
917
|
-
);
|
|
1071
|
+
const newBgParts = await Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
|
|
918
1072
|
pseudoEl.style.backgroundImage = newBgParts.join(", ");
|
|
919
1073
|
} catch (e) {
|
|
920
1074
|
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
921
1075
|
}
|
|
922
1076
|
}
|
|
923
1077
|
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
924
|
-
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent
|
|
1078
|
+
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
|
|
925
1079
|
const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
|
|
926
1080
|
if (!hasVisibleBox) continue;
|
|
927
|
-
pseudo === "::before"
|
|
1081
|
+
if (pseudo === "::before") {
|
|
1082
|
+
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
1083
|
+
} else {
|
|
1084
|
+
clone.appendChild(pseudoEl);
|
|
1085
|
+
}
|
|
928
1086
|
} catch (e) {
|
|
929
1087
|
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
930
1088
|
}
|
|
@@ -932,84 +1090,111 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
|
|
|
932
1090
|
const sChildren = Array.from(source.children);
|
|
933
1091
|
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
934
1092
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
935
|
-
await inlinePseudoElements(
|
|
936
|
-
sChildren[i],
|
|
937
|
-
cChildren[i],
|
|
938
|
-
compress,
|
|
939
|
-
embedFonts,
|
|
940
|
-
useProxy
|
|
941
|
-
);
|
|
1093
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, options);
|
|
942
1094
|
}
|
|
943
1095
|
}
|
|
944
1096
|
|
|
945
1097
|
// src/modules/svgDefs.js
|
|
946
|
-
function
|
|
947
|
-
if (!
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1098
|
+
function inlineExternalDefsAndSymbols(rootElement) {
|
|
1099
|
+
if (!rootElement) return;
|
|
1100
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1101
|
+
rootElement.querySelectorAll("use").forEach((use) => {
|
|
1102
|
+
const href = use.getAttribute("xlink:href") || use.getAttribute("href");
|
|
1103
|
+
if (href && href.startsWith("#")) {
|
|
1104
|
+
usedIds.add(href.slice(1));
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
if (!usedIds.size) return;
|
|
1108
|
+
const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
|
|
1109
|
+
const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
|
|
1110
|
+
const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
|
|
1111
|
+
let container = rootElement.querySelector("svg.inline-defs-container");
|
|
1112
|
+
if (!container) {
|
|
1113
|
+
container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1114
|
+
container.setAttribute("aria-hidden", "true");
|
|
1115
|
+
container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
|
|
1116
|
+
container.classList.add("inline-defs-container");
|
|
1117
|
+
rootElement.insertBefore(container, rootElement.firstChild);
|
|
1118
|
+
}
|
|
1119
|
+
const existingIds = /* @__PURE__ */ new Set();
|
|
1120
|
+
rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
|
|
1121
|
+
existingIds.add(el.id);
|
|
1122
|
+
});
|
|
1123
|
+
usedIds.forEach((id) => {
|
|
1124
|
+
if (existingIds.has(id)) return;
|
|
1125
|
+
const symbol = globalSymbols.find((sym) => sym.id === id);
|
|
1126
|
+
if (symbol) {
|
|
1127
|
+
container.appendChild(symbol.cloneNode(true));
|
|
1128
|
+
existingIds.add(id);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
for (const defs of globalDefs) {
|
|
1132
|
+
const defEl = defs.querySelector(`#${CSS.escape(id)}`);
|
|
1133
|
+
if (defEl) {
|
|
1134
|
+
let defsContainer = container.querySelector("defs");
|
|
1135
|
+
if (!defsContainer) {
|
|
1136
|
+
defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
1137
|
+
container.appendChild(defsContainer);
|
|
966
1138
|
}
|
|
1139
|
+
defsContainer.appendChild(defEl.cloneNode(true));
|
|
1140
|
+
existingIds.add(id);
|
|
1141
|
+
break;
|
|
967
1142
|
}
|
|
968
1143
|
}
|
|
969
|
-
if (defs.childNodes.length) {
|
|
970
|
-
svg.insertBefore(defs, svg.firstChild);
|
|
971
|
-
}
|
|
972
1144
|
});
|
|
973
1145
|
}
|
|
974
1146
|
|
|
975
1147
|
// src/core/prepare.js
|
|
976
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();
|
|
977
1152
|
let clone;
|
|
978
1153
|
let classCSS = "";
|
|
1154
|
+
stabilizeLayout(element);
|
|
979
1155
|
try {
|
|
980
|
-
|
|
1156
|
+
inlineExternalDefsAndSymbols(element);
|
|
981
1157
|
} catch (e) {
|
|
982
|
-
console.warn("
|
|
983
|
-
throw e;
|
|
1158
|
+
console.warn("inlineExternal defs or symbol failed:", e);
|
|
984
1159
|
}
|
|
985
1160
|
try {
|
|
986
|
-
|
|
1161
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
|
|
987
1162
|
} catch (e) {
|
|
988
|
-
console.warn("
|
|
1163
|
+
console.warn("deepClone failed:", e);
|
|
1164
|
+
throw e;
|
|
989
1165
|
}
|
|
990
1166
|
try {
|
|
991
|
-
|
|
1167
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
|
|
992
1168
|
} catch (e) {
|
|
993
|
-
console.warn("
|
|
1169
|
+
console.warn("inlinePseudoElements failed:", e);
|
|
994
1170
|
}
|
|
1171
|
+
await resolveBlobUrlsInTree(clone);
|
|
995
1172
|
if (compress) {
|
|
996
|
-
const keyToClass = generateCSSClasses();
|
|
1173
|
+
const keyToClass = generateCSSClasses(styleMap);
|
|
997
1174
|
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
998
|
-
for (const [node, key] of
|
|
1175
|
+
for (const [node, key] of styleMap.entries()) {
|
|
999
1176
|
if (node.tagName === "STYLE") continue;
|
|
1177
|
+
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
1178
|
+
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1000
1181
|
const className = keyToClass.get(key);
|
|
1001
1182
|
if (className) node.classList.add(className);
|
|
1002
1183
|
const bgImage = node.style?.backgroundImage;
|
|
1003
|
-
node.
|
|
1184
|
+
const hasIcon = node.dataset?.snapdomHasIcon;
|
|
1004
1185
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
1186
|
+
if (hasIcon) {
|
|
1187
|
+
node.style.verticalAlign = "middle";
|
|
1188
|
+
node.style.display = "inline";
|
|
1189
|
+
}
|
|
1005
1190
|
}
|
|
1006
1191
|
} else {
|
|
1007
|
-
for (const [node, key] of
|
|
1192
|
+
for (const [node, key] of styleMap.entries()) {
|
|
1008
1193
|
if (node.tagName === "STYLE") continue;
|
|
1009
1194
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1010
1195
|
}
|
|
1011
1196
|
}
|
|
1012
|
-
for (const [cloneNode, originalNode] of
|
|
1197
|
+
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1013
1198
|
const scrollX = originalNode.scrollLeft;
|
|
1014
1199
|
const scrollY = originalNode.scrollTop;
|
|
1015
1200
|
const hasScroll = scrollX || scrollY;
|
|
@@ -1028,9 +1213,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1028
1213
|
cloneNode.appendChild(inner);
|
|
1029
1214
|
}
|
|
1030
1215
|
}
|
|
1031
|
-
if (element ===
|
|
1032
|
-
const computed =
|
|
1033
|
-
|
|
1216
|
+
if (element === nodeMap.get(clone)) {
|
|
1217
|
+
const computed = styleCache.get(element) || window.getComputedStyle(element);
|
|
1218
|
+
styleCache.set(element, computed);
|
|
1034
1219
|
const transform = stripTranslate(computed.transform);
|
|
1035
1220
|
clone.style.margin = "0";
|
|
1036
1221
|
clone.style.position = "static";
|
|
@@ -1043,19 +1228,160 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1043
1228
|
clone.style.clear = "none";
|
|
1044
1229
|
clone.style.transform = transform || "";
|
|
1045
1230
|
}
|
|
1046
|
-
for (const [cloneNode, originalNode] of
|
|
1231
|
+
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1047
1232
|
if (originalNode.tagName === "PRE") {
|
|
1048
1233
|
cloneNode.style.marginTop = "0";
|
|
1049
1234
|
cloneNode.style.marginBlockStart = "0";
|
|
1050
1235
|
}
|
|
1051
1236
|
}
|
|
1052
|
-
return { clone, classCSS };
|
|
1237
|
+
return { clone, classCSS, styleCache };
|
|
1238
|
+
}
|
|
1239
|
+
function stabilizeLayout(element) {
|
|
1240
|
+
const style = getComputedStyle(element);
|
|
1241
|
+
const outlineStyle = style.outlineStyle;
|
|
1242
|
+
const outlineWidth = style.outlineWidth;
|
|
1243
|
+
const borderStyle = style.borderStyle;
|
|
1244
|
+
const borderWidth = style.borderWidth;
|
|
1245
|
+
const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
|
|
1246
|
+
const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
|
|
1247
|
+
if (outlineVisible && borderAbsent) {
|
|
1248
|
+
element.style.border = `${outlineWidth} solid transparent`;
|
|
1249
|
+
}
|
|
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
|
+
}
|
|
1053
1373
|
}
|
|
1054
1374
|
|
|
1055
1375
|
// src/modules/images.js
|
|
1056
1376
|
async function inlineImages(clone, options = {}) {
|
|
1057
1377
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
1058
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");
|
|
1059
1385
|
const src = img.src;
|
|
1060
1386
|
try {
|
|
1061
1387
|
const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
|
|
@@ -1076,7 +1402,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
1076
1402
|
}
|
|
1077
1403
|
|
|
1078
1404
|
// src/modules/background.js
|
|
1079
|
-
async function inlineBackgroundImages(source, clone, options = {}) {
|
|
1405
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
1080
1406
|
const queue = [[source, clone]];
|
|
1081
1407
|
const imageProps = [
|
|
1082
1408
|
"background-image",
|
|
@@ -1086,13 +1412,27 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1086
1412
|
"mask-source",
|
|
1087
1413
|
"mask-box-image-source",
|
|
1088
1414
|
"mask-border-source",
|
|
1089
|
-
"-webkit-mask-box-image-source"
|
|
1415
|
+
"-webkit-mask-box-image-source",
|
|
1416
|
+
"border-image",
|
|
1417
|
+
"border-image-source",
|
|
1418
|
+
"border-image-slice",
|
|
1419
|
+
"border-image-width",
|
|
1420
|
+
"border-image-outset",
|
|
1421
|
+
"border-image-repeat"
|
|
1090
1422
|
];
|
|
1091
1423
|
while (queue.length) {
|
|
1092
1424
|
const [srcNode, cloneNode] = queue.shift();
|
|
1093
|
-
const style =
|
|
1094
|
-
if (!
|
|
1425
|
+
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
1426
|
+
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
1427
|
+
const hasBorderImage = (() => {
|
|
1428
|
+
const bi = style.getPropertyValue("border-image");
|
|
1429
|
+
const bis = style.getPropertyValue("border-image-source");
|
|
1430
|
+
return bi && bi !== "none" || bis && bis !== "none";
|
|
1431
|
+
})();
|
|
1095
1432
|
for (const prop of imageProps) {
|
|
1433
|
+
if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1096
1436
|
const val = style.getPropertyValue(prop);
|
|
1097
1437
|
if (!val || val === "none") continue;
|
|
1098
1438
|
const splits = splitBackgroundImage(val);
|
|
@@ -1119,13 +1459,13 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1119
1459
|
async function captureDOM(element, options = {}) {
|
|
1120
1460
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1121
1461
|
cache.reset();
|
|
1122
|
-
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
1123
|
-
let clone, classCSS;
|
|
1462
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "", localFonts = [] } = options;
|
|
1463
|
+
let clone, classCSS, styleCache;
|
|
1124
1464
|
let fontsCSS = "";
|
|
1125
1465
|
let baseCSS = "";
|
|
1126
1466
|
let dataURL;
|
|
1127
1467
|
let svgString;
|
|
1128
|
-
({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));
|
|
1468
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
|
|
1129
1469
|
await new Promise((resolve) => {
|
|
1130
1470
|
idle(async () => {
|
|
1131
1471
|
await inlineImages(clone, options);
|
|
@@ -1134,14 +1474,14 @@ async function captureDOM(element, options = {}) {
|
|
|
1134
1474
|
});
|
|
1135
1475
|
await new Promise((resolve) => {
|
|
1136
1476
|
idle(async () => {
|
|
1137
|
-
await inlineBackgroundImages(element, clone, options);
|
|
1477
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
1138
1478
|
resolve();
|
|
1139
1479
|
}, { fast });
|
|
1140
1480
|
});
|
|
1141
1481
|
if (embedFonts) {
|
|
1142
1482
|
await new Promise((resolve) => {
|
|
1143
1483
|
idle(async () => {
|
|
1144
|
-
fontsCSS = await embedCustomFonts();
|
|
1484
|
+
fontsCSS = await embedCustomFonts({ localFonts, useProxy });
|
|
1145
1485
|
resolve();
|
|
1146
1486
|
}, { fast });
|
|
1147
1487
|
});
|
|
@@ -1218,12 +1558,14 @@ async function captureDOM(element, options = {}) {
|
|
|
1218
1558
|
}
|
|
1219
1559
|
|
|
1220
1560
|
// src/api/snapdom.js
|
|
1221
|
-
async function toImg(url, {
|
|
1561
|
+
async function toImg(url, { scale = 1 } = {}) {
|
|
1222
1562
|
const img = new Image();
|
|
1223
1563
|
img.src = url;
|
|
1224
1564
|
await img.decode();
|
|
1225
|
-
|
|
1226
|
-
|
|
1565
|
+
if (scale !== 1) {
|
|
1566
|
+
img.style.width = `${img.naturalWidth * scale}px`;
|
|
1567
|
+
img.style.height = `${img.naturalHeight * scale}px`;
|
|
1568
|
+
}
|
|
1227
1569
|
return img;
|
|
1228
1570
|
}
|
|
1229
1571
|
async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
@@ -1246,8 +1588,8 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1246
1588
|
if (appended) img.remove();
|
|
1247
1589
|
throw new Error("Image failed to load or has no dimensions");
|
|
1248
1590
|
}
|
|
1249
|
-
const width = img.
|
|
1250
|
-
const height = img.
|
|
1591
|
+
const width = img.naturalWidth * scale;
|
|
1592
|
+
const height = img.naturalHeight * scale;
|
|
1251
1593
|
const canvas = document.createElement("canvas");
|
|
1252
1594
|
canvas.width = Math.ceil(width * dpr);
|
|
1253
1595
|
canvas.height = Math.ceil(height * dpr);
|
|
@@ -1339,19 +1681,19 @@ async function snapdom(element, options = {}) {
|
|
|
1339
1681
|
}
|
|
1340
1682
|
snapdom.capture = async (el, options = {}) => {
|
|
1341
1683
|
const url = await captureDOM(el, options);
|
|
1342
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1684
|
+
const dpr = options.dpr ?? (window.devicePixelRatio || 1);
|
|
1343
1685
|
const scale = options.scale || 1;
|
|
1344
1686
|
return {
|
|
1345
1687
|
url,
|
|
1346
1688
|
options,
|
|
1347
1689
|
toRaw: () => url,
|
|
1348
|
-
toImg: () => toImg(url, { dpr, scale }),
|
|
1349
|
-
toCanvas: () => toCanvas(url, { dpr, scale }),
|
|
1350
|
-
toBlob: (
|
|
1351
|
-
toPng: (
|
|
1352
|
-
toJpg: (
|
|
1353
|
-
toWebp: (
|
|
1354
|
-
download: ({ format = "png", filename = "
|
|
1690
|
+
toImg: (opts = {}) => toImg(url, { dpr, scale, ...opts }),
|
|
1691
|
+
toCanvas: (opts = {}) => toCanvas(url, { dpr, scale, ...opts }),
|
|
1692
|
+
toBlob: (opts = {}) => toBlob(url, { dpr, scale, ...opts }),
|
|
1693
|
+
toPng: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "png"),
|
|
1694
|
+
toJpg: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "jpeg"),
|
|
1695
|
+
toWebp: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "webp"),
|
|
1696
|
+
download: ({ format = "png", filename = "snapDOM", backgroundColor, ...opts } = {}) => download(url, { dpr, scale, format, filename, backgroundColor, ...opts })
|
|
1355
1697
|
};
|
|
1356
1698
|
};
|
|
1357
1699
|
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
|
|
@@ -1374,12 +1716,21 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1374
1716
|
|
|
1375
1717
|
// src/api/preCache.js
|
|
1376
1718
|
async function preCache(root = document, options = {}) {
|
|
1377
|
-
const { embedFonts = true, reset = false } = options;
|
|
1719
|
+
const { embedFonts = true, reset = false, useProxy } = options;
|
|
1378
1720
|
if (reset) {
|
|
1379
|
-
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();
|
|
1380
1728
|
return;
|
|
1381
1729
|
}
|
|
1382
|
-
|
|
1730
|
+
try {
|
|
1731
|
+
await document.fonts.ready;
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1383
1734
|
precacheCommonTags();
|
|
1384
1735
|
let imgEls = [], allEls = [];
|
|
1385
1736
|
if (root?.querySelectorAll) {
|
|
@@ -1388,33 +1739,41 @@ async function preCache(root = document, options = {}) {
|
|
|
1388
1739
|
}
|
|
1389
1740
|
const promises = [];
|
|
1390
1741
|
for (const img of imgEls) {
|
|
1391
|
-
const src = img
|
|
1742
|
+
const src = img?.src;
|
|
1743
|
+
if (!src) continue;
|
|
1392
1744
|
if (!cache.image.has(src)) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
);
|
|
1745
|
+
const p = Promise.resolve().then(() => fetchImage(src, { useProxy })).then((dataURL) => {
|
|
1746
|
+
cache.image.set(src, dataURL);
|
|
1747
|
+
}).catch(() => {
|
|
1748
|
+
});
|
|
1749
|
+
promises.push(p);
|
|
1397
1750
|
}
|
|
1398
1751
|
}
|
|
1399
1752
|
for (const el of allEls) {
|
|
1400
|
-
|
|
1753
|
+
let bg = "";
|
|
1754
|
+
try {
|
|
1755
|
+
bg = getStyle(el).backgroundImage;
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1401
1758
|
if (bg && bg !== "none") {
|
|
1402
1759
|
const bgSplits = splitBackgroundImage(bg);
|
|
1403
1760
|
for (const entry of bgSplits) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
})
|
|
1409
|
-
);
|
|
1761
|
+
if (entry.startsWith("url(")) {
|
|
1762
|
+
const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
|
|
1763
|
+
});
|
|
1764
|
+
promises.push(p);
|
|
1410
1765
|
}
|
|
1411
1766
|
}
|
|
1412
1767
|
}
|
|
1413
1768
|
}
|
|
1414
1769
|
if (embedFonts) {
|
|
1415
|
-
|
|
1770
|
+
try {
|
|
1771
|
+
await embedCustomFonts({ preCached: true, localFonts: options.localFonts, useProxy: options.useProxy });
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
1774
|
+
;
|
|
1416
1775
|
}
|
|
1417
|
-
await Promise.
|
|
1776
|
+
await Promise.allSettled(promises);
|
|
1418
1777
|
}
|
|
1419
1778
|
export {
|
|
1420
1779
|
preCache,
|