@zumer/snapdom 1.7.1 → 1.9.3-next-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -35
- package/dist/snapdom.js +1089 -598
- package/dist/snapdom.min.js +13 -13
- package/dist/snapdom.min.mjs +13 -13
- package/dist/snapdom.mjs +559 -171
- package/package.json +3 -2
- package/types/snapdom.d.ts +7 -2
package/dist/snapdom.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.
|
|
4
|
+
* v.1.9.3-next-dev.0
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
// src/core/cache.js
|
|
10
11
|
var imageCache = /* @__PURE__ */ new Map();
|
|
11
12
|
var bgCache = /* @__PURE__ */ new Map();
|
|
@@ -13,6 +14,7 @@ var resourceCache = /* @__PURE__ */ new Map();
|
|
|
13
14
|
var defaultStylesCache = /* @__PURE__ */ new Map();
|
|
14
15
|
var baseCSSCache = /* @__PURE__ */ new Map();
|
|
15
16
|
var computedStyleCache = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
var processedFontURLs = /* @__PURE__ */ new Set();
|
|
16
18
|
|
|
17
19
|
// src/utils/cssTools.js
|
|
18
20
|
var commonTags = [
|
|
@@ -152,6 +154,25 @@ function generateCSSClasses(styleMap) {
|
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
// src/utils/helpers.js
|
|
157
|
+
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
158
|
+
const match = entry.match(/url\(["']?(.*?)["']?\)/);
|
|
159
|
+
const rawUrl = match?.[1];
|
|
160
|
+
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
161
|
+
if (rawUrl) {
|
|
162
|
+
const encodedUrl = safeEncodeURI(rawUrl);
|
|
163
|
+
if (bgCache.has(encodedUrl)) {
|
|
164
|
+
return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
|
|
165
|
+
} else {
|
|
166
|
+
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
167
|
+
bgCache.set(encodedUrl, dataUrl);
|
|
168
|
+
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (isGradient || entry === "none") {
|
|
172
|
+
return entry;
|
|
173
|
+
}
|
|
174
|
+
return entry;
|
|
175
|
+
}
|
|
155
176
|
function idle(fn, { fast = false } = {}) {
|
|
156
177
|
if (fast) return fn();
|
|
157
178
|
if ("requestIdleCallback" in window) {
|
|
@@ -196,29 +217,84 @@ function extractURL(value) {
|
|
|
196
217
|
}
|
|
197
218
|
return url;
|
|
198
219
|
}
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
221
|
+
function getCrossOriginMode(url) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = new URL(url, window.location.href);
|
|
224
|
+
return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
|
|
225
|
+
} catch {
|
|
226
|
+
return "anonymous";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function fetchWithFallback(url) {
|
|
230
|
+
const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
|
|
231
|
+
mode: "cors",
|
|
232
|
+
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
233
|
+
}).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
|
|
234
|
+
const reader = new FileReader();
|
|
235
|
+
reader.onloadend = () => {
|
|
236
|
+
const base64 = reader.result;
|
|
237
|
+
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
238
|
+
reject(new Error("Invalid image data URL"));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
resolve(base64);
|
|
242
|
+
};
|
|
243
|
+
reader.onerror = () => reject(new Error("FileReader error"));
|
|
244
|
+
reader.readAsDataURL(blob);
|
|
245
|
+
}));
|
|
246
|
+
try {
|
|
247
|
+
return await fetchBlobAsDataURL(url);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (useProxy && typeof useProxy === "string") {
|
|
250
|
+
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
251
|
+
try {
|
|
252
|
+
return await fetchBlobAsDataURL(proxied);
|
|
253
|
+
} catch {
|
|
254
|
+
console.error(`[SnapDOM - fetchImage] Proxy fallback failed for: ${url}`);
|
|
255
|
+
throw new Error("CORS restrictions prevented image capture (even via proxy)");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
console.error(`[SnapDOM - fetchImage] No valid proxy URL provided for fallback: ${url}`);
|
|
259
|
+
throw new Error("Fetch fallback failed and no proxy provided");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const crossOriginValue = getCrossOriginMode(src);
|
|
264
|
+
console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
|
|
213
265
|
if (imageCache.has(src)) {
|
|
266
|
+
console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
|
|
214
267
|
return Promise.resolve(imageCache.get(src));
|
|
215
268
|
}
|
|
269
|
+
const isDataURI = src.startsWith("data:image/");
|
|
270
|
+
if (isDataURI) {
|
|
271
|
+
imageCache.set(src, src);
|
|
272
|
+
return Promise.resolve(src);
|
|
273
|
+
}
|
|
274
|
+
const isSVG = /\.svg(\?.*)?$/i.test(src);
|
|
275
|
+
if (isSVG) {
|
|
276
|
+
return (async () => {
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(src, {
|
|
279
|
+
mode: "cors",
|
|
280
|
+
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
281
|
+
});
|
|
282
|
+
const svgText = await response.text();
|
|
283
|
+
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
284
|
+
imageCache.set(src, encoded);
|
|
285
|
+
return encoded;
|
|
286
|
+
} catch {
|
|
287
|
+
return fetchWithFallback(src);
|
|
288
|
+
}
|
|
289
|
+
})();
|
|
290
|
+
}
|
|
216
291
|
return new Promise((resolve, reject) => {
|
|
217
292
|
const timeoutId = setTimeout(() => {
|
|
293
|
+
console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
|
|
218
294
|
reject(new Error("Image load timed out"));
|
|
219
295
|
}, timeout);
|
|
220
296
|
const image = new Image();
|
|
221
|
-
image.crossOrigin =
|
|
297
|
+
image.crossOrigin = crossOriginValue;
|
|
222
298
|
image.onload = async () => {
|
|
223
299
|
clearTimeout(timeoutId);
|
|
224
300
|
try {
|
|
@@ -228,20 +304,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
|
228
304
|
canvas.height = image.height;
|
|
229
305
|
const ctx = canvas.getContext("2d");
|
|
230
306
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
307
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
308
|
+
imageCache.set(src, dataURL);
|
|
309
|
+
resolve(dataURL);
|
|
310
|
+
} catch {
|
|
231
311
|
try {
|
|
232
|
-
const
|
|
233
|
-
imageCache.set(src,
|
|
234
|
-
resolve(
|
|
312
|
+
const fallbackDataURL = await fetchWithFallback(src);
|
|
313
|
+
imageCache.set(src, fallbackDataURL);
|
|
314
|
+
resolve(fallbackDataURL);
|
|
235
315
|
} catch (e) {
|
|
236
|
-
reject(
|
|
316
|
+
reject(e);
|
|
237
317
|
}
|
|
238
|
-
} catch (e) {
|
|
239
|
-
reject(e);
|
|
240
318
|
}
|
|
241
319
|
};
|
|
242
|
-
image.onerror = (
|
|
320
|
+
image.onerror = async () => {
|
|
243
321
|
clearTimeout(timeoutId);
|
|
244
|
-
|
|
322
|
+
console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
|
|
323
|
+
try {
|
|
324
|
+
const fallbackDataURL = await fetchWithFallback(src);
|
|
325
|
+
imageCache.set(src, fallbackDataURL);
|
|
326
|
+
resolve(fallbackDataURL);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
reject(e);
|
|
329
|
+
}
|
|
245
330
|
};
|
|
246
331
|
image.src = src;
|
|
247
332
|
});
|
|
@@ -256,35 +341,135 @@ function snapshotComputedStyle(style) {
|
|
|
256
341
|
function isSafari() {
|
|
257
342
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
258
343
|
}
|
|
344
|
+
function stripTranslate(transform) {
|
|
345
|
+
if (!transform || transform === "none") return "";
|
|
346
|
+
let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
|
|
347
|
+
cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
|
|
348
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
349
|
+
if (parts.length !== 6) return `matrix(${values})`;
|
|
350
|
+
parts[4] = "0";
|
|
351
|
+
parts[5] = "0";
|
|
352
|
+
return `matrix(${parts.join(", ")})`;
|
|
353
|
+
});
|
|
354
|
+
cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
|
|
355
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
356
|
+
if (parts.length !== 16) return `matrix3d(${values})`;
|
|
357
|
+
parts[12] = "0";
|
|
358
|
+
parts[13] = "0";
|
|
359
|
+
return `matrix3d(${parts.join(", ")})`;
|
|
360
|
+
});
|
|
361
|
+
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
362
|
+
}
|
|
363
|
+
function safeEncodeURI(uri) {
|
|
364
|
+
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
365
|
+
try {
|
|
366
|
+
return encodeURI(uri);
|
|
367
|
+
} catch {
|
|
368
|
+
return uri;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function splitBackgroundImage(bg) {
|
|
372
|
+
const parts = [];
|
|
373
|
+
let depth = 0;
|
|
374
|
+
let lastIndex = 0;
|
|
375
|
+
for (let i = 0; i < bg.length; i++) {
|
|
376
|
+
const char = bg[i];
|
|
377
|
+
if (char === "(") depth++;
|
|
378
|
+
if (char === ")") depth--;
|
|
379
|
+
if (char === "," && depth === 0) {
|
|
380
|
+
parts.push(bg.slice(lastIndex, i).trim());
|
|
381
|
+
lastIndex = i + 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
parts.push(bg.slice(lastIndex).trim());
|
|
385
|
+
return parts;
|
|
386
|
+
}
|
|
259
387
|
|
|
260
388
|
// src/modules/styles.js
|
|
389
|
+
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
390
|
+
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
391
|
+
function snapshotComputedStyleFull(style) {
|
|
392
|
+
const result = {};
|
|
393
|
+
for (let i = 0; i < style.length; i++) {
|
|
394
|
+
const prop = style[i];
|
|
395
|
+
let val = style.getPropertyValue(prop);
|
|
396
|
+
if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
|
|
397
|
+
val = "none";
|
|
398
|
+
}
|
|
399
|
+
result[prop] = val;
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
261
403
|
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
262
404
|
if (source.tagName === "STYLE") return;
|
|
263
405
|
if (!cache.has(source)) {
|
|
264
406
|
cache.set(source, getStyle(source));
|
|
265
407
|
}
|
|
266
408
|
const style = cache.get(source);
|
|
267
|
-
|
|
409
|
+
if (!snapshotCache.has(source)) {
|
|
410
|
+
const snapshot2 = snapshotComputedStyleFull(style);
|
|
411
|
+
snapshotCache.set(source, snapshot2);
|
|
412
|
+
}
|
|
413
|
+
const snapshot = snapshotCache.get(source);
|
|
414
|
+
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
415
|
+
if (snapshotKeyCache.has(hash)) {
|
|
416
|
+
styleMap.set(clone, snapshotKeyCache.get(hash));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
268
419
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
269
420
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
421
|
+
snapshotKeyCache.set(hash, key);
|
|
270
422
|
styleMap.set(clone, key);
|
|
271
423
|
}
|
|
272
424
|
|
|
273
425
|
// src/core/clone.js
|
|
274
|
-
function
|
|
275
|
-
|
|
426
|
+
function isSlotElement(node) {
|
|
427
|
+
return node.nodeType === Node.ELEMENT_NODE && node.tagName === "SLOT";
|
|
428
|
+
}
|
|
429
|
+
function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
|
|
430
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
431
|
+
return node.cloneNode(true);
|
|
432
|
+
}
|
|
433
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
434
|
+
if (node.getAttribute("data-capture") === "exclude") {
|
|
276
435
|
const spacer = document.createElement("div");
|
|
277
436
|
const rect = node.getBoundingClientRect();
|
|
278
437
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
279
438
|
return spacer;
|
|
280
439
|
}
|
|
440
|
+
if (options.exclude && Array.isArray(options.exclude)) {
|
|
441
|
+
for (const selector of options.exclude) {
|
|
442
|
+
try {
|
|
443
|
+
if (node.matches?.(selector)) {
|
|
444
|
+
const spacer = document.createElement("div");
|
|
445
|
+
const rect = node.getBoundingClientRect();
|
|
446
|
+
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
447
|
+
return spacer;
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.warn(`Invalid selector in exclude option: ${selector}`, err);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (typeof options.filter === "function") {
|
|
455
|
+
try {
|
|
456
|
+
if (!options.filter(node, originalRoot || node)) {
|
|
457
|
+
const spacer = document.createElement("div");
|
|
458
|
+
const rect = node.getBoundingClientRect();
|
|
459
|
+
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
460
|
+
return spacer;
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.warn("Error in filter function:", err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
281
466
|
if (node.tagName === "IFRAME") {
|
|
282
467
|
const fallback = document.createElement("div");
|
|
283
468
|
fallback.textContent = "";
|
|
284
|
-
fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`;
|
|
469
|
+
fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background-image: repeating-linear-gradient(45deg, #ddd, #ddd 5px, #f9f9f9 5px, #f9f9f9 10px);display: flex;align-items: center;justify-content: center;font-size: 12px;color: #555; border: 1px solid #aaa;`;
|
|
285
470
|
return fallback;
|
|
286
471
|
}
|
|
287
|
-
if (node.
|
|
472
|
+
if (node.getAttribute("data-capture") === "placeholder") {
|
|
288
473
|
const clone2 = node.cloneNode(false);
|
|
289
474
|
nodeMap.set(clone2, node);
|
|
290
475
|
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
@@ -301,18 +486,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
301
486
|
img.width = node.width;
|
|
302
487
|
img.height = node.height;
|
|
303
488
|
img.style.display = "inline-block";
|
|
304
|
-
img.style.width = `${node.width}px`;
|
|
305
|
-
img.style.height = `${node.height}px`;
|
|
489
|
+
img.style.width = node.style.width || `${node.width}px`;
|
|
490
|
+
img.style.height = node.style.height || `${node.height}px`;
|
|
306
491
|
return img;
|
|
307
492
|
}
|
|
308
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
309
|
-
if (node.parentElement?.shadowRoot) {
|
|
310
|
-
const tag = node.parentElement.tagName.toLowerCase();
|
|
311
|
-
if (customElements.get(tag)) return null;
|
|
312
|
-
}
|
|
313
|
-
return node.cloneNode(true);
|
|
314
|
-
}
|
|
315
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
316
493
|
const clone = node.cloneNode(false);
|
|
317
494
|
nodeMap.set(clone, node);
|
|
318
495
|
if (node instanceof HTMLInputElement) {
|
|
@@ -323,8 +500,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
323
500
|
if (node.checked) clone.setAttribute("checked", "");
|
|
324
501
|
}
|
|
325
502
|
} else if (node instanceof HTMLTextAreaElement) {
|
|
326
|
-
|
|
503
|
+
const rect = node.getBoundingClientRect();
|
|
327
504
|
clone.textContent = node.value;
|
|
505
|
+
clone.style.width = `${rect.width}px`;
|
|
506
|
+
clone.style.height = `${rect.height}px`;
|
|
328
507
|
} else if (node instanceof HTMLSelectElement) {
|
|
329
508
|
clone.value = node.value;
|
|
330
509
|
Array.from(clone.options).forEach((opt) => {
|
|
@@ -336,21 +515,71 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
336
515
|
});
|
|
337
516
|
}
|
|
338
517
|
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
518
|
+
if (isSlotElement(node)) {
|
|
519
|
+
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
520
|
+
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
521
|
+
const fragment = document.createDocumentFragment();
|
|
522
|
+
for (const child of nodesToClone) {
|
|
523
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
524
|
+
if (clonedChild) fragment.appendChild(clonedChild);
|
|
525
|
+
}
|
|
526
|
+
return fragment;
|
|
527
|
+
} else if (node instanceof HTMLTextAreaElement) {
|
|
528
|
+
} else {
|
|
529
|
+
const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
|
|
530
|
+
for (const child of baseChildren) {
|
|
531
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
532
|
+
if (clonedChild) clone.appendChild(clonedChild);
|
|
533
|
+
}
|
|
534
|
+
if (node.shadowRoot && node.childNodes.length > 0 && !node.shadowRoot.querySelector("slot")) {
|
|
535
|
+
const lightDomContent = document.createDocumentFragment();
|
|
536
|
+
for (const child of node.childNodes) {
|
|
537
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, originalRoot || node);
|
|
538
|
+
if (clonedChild) lightDomContent.appendChild(clonedChild);
|
|
539
|
+
}
|
|
540
|
+
clone.appendChild(lightDomContent);
|
|
541
|
+
}
|
|
350
542
|
}
|
|
351
543
|
return clone;
|
|
352
544
|
}
|
|
353
545
|
|
|
546
|
+
// src/modules/iconFonts.js
|
|
547
|
+
var defaultIconFonts = [
|
|
548
|
+
// /uicons/i,
|
|
549
|
+
/font\s*awesome/i,
|
|
550
|
+
/material\s*icons/i,
|
|
551
|
+
/ionicons/i,
|
|
552
|
+
/glyphicons/i,
|
|
553
|
+
/feather/i,
|
|
554
|
+
/bootstrap\s*icons/i,
|
|
555
|
+
/remix\s*icons/i,
|
|
556
|
+
/heroicons/i,
|
|
557
|
+
/layui/i,
|
|
558
|
+
/lucide/i
|
|
559
|
+
];
|
|
560
|
+
var userIconFonts = [];
|
|
561
|
+
function extendIconFonts(fonts) {
|
|
562
|
+
const list = Array.isArray(fonts) ? fonts : [fonts];
|
|
563
|
+
for (const f of list) {
|
|
564
|
+
if (f instanceof RegExp) {
|
|
565
|
+
userIconFonts.push(f);
|
|
566
|
+
} else if (typeof f === "string") {
|
|
567
|
+
userIconFonts.push(new RegExp(f, "i"));
|
|
568
|
+
} else {
|
|
569
|
+
console.warn("[snapdom] Ignored invalid iconFont value:", f);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function isIconFont(input) {
|
|
574
|
+
const text = typeof input === "string" ? input : "";
|
|
575
|
+
const candidates = [...defaultIconFonts, ...userIconFonts];
|
|
576
|
+
for (const rx of candidates) {
|
|
577
|
+
if (rx instanceof RegExp && rx.test(text)) return true;
|
|
578
|
+
}
|
|
579
|
+
if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
354
583
|
// src/modules/fonts.js
|
|
355
584
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
356
585
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
@@ -375,36 +604,67 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
375
604
|
ctx.fillText(unicodeChar, 0, ascent);
|
|
376
605
|
return canvas.toDataURL();
|
|
377
606
|
}
|
|
378
|
-
|
|
607
|
+
function isStylesheetLoaded(href) {
|
|
608
|
+
return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
|
|
609
|
+
}
|
|
610
|
+
function injectLinkIfMissing(href) {
|
|
611
|
+
return new Promise((resolve) => {
|
|
612
|
+
if (isStylesheetLoaded(href)) return resolve(null);
|
|
613
|
+
const link = document.createElement("link");
|
|
614
|
+
link.rel = "stylesheet";
|
|
615
|
+
link.href = href;
|
|
616
|
+
link.setAttribute("data-snapdom", "injected-import");
|
|
617
|
+
link.onload = () => resolve(link);
|
|
618
|
+
link.onerror = () => resolve(null);
|
|
619
|
+
document.head.appendChild(link);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async function embedCustomFonts({ preCached = false } = {}) {
|
|
623
|
+
if (resourceCache.has("fonts-embed-css")) {
|
|
624
|
+
if (preCached) {
|
|
625
|
+
const style = document.createElement("style");
|
|
626
|
+
style.setAttribute("data-snapdom", "embedFonts");
|
|
627
|
+
style.textContent = resourceCache.get("fonts-embed-css");
|
|
628
|
+
document.head.appendChild(style);
|
|
629
|
+
}
|
|
630
|
+
return resourceCache.get("fonts-embed-css");
|
|
631
|
+
}
|
|
632
|
+
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
633
|
+
const styleImports = [];
|
|
634
|
+
for (const styleTag of document.querySelectorAll("style")) {
|
|
635
|
+
const cssText = styleTag.textContent || "";
|
|
636
|
+
const matches = Array.from(cssText.matchAll(importRegex));
|
|
637
|
+
for (const match of matches) {
|
|
638
|
+
const importUrl = match[1];
|
|
639
|
+
if (isIconFont(importUrl)) continue;
|
|
640
|
+
if (!isStylesheetLoaded(importUrl)) {
|
|
641
|
+
styleImports.push(importUrl);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
await Promise.all(styleImports.map(injectLinkIfMissing));
|
|
379
646
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
|
|
380
647
|
let finalCSS = "";
|
|
381
648
|
for (const link of links) {
|
|
382
649
|
try {
|
|
383
650
|
const res = await fetch(link.href);
|
|
384
651
|
const cssText = await res.text();
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
const urlRegex = /url\(([^)]+)\)/g;
|
|
652
|
+
if (isIconFont(link.href) || isIconFont(cssText)) continue;
|
|
653
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
389
654
|
const inlinedCSS = await Promise.all(
|
|
390
655
|
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
|
|
391
656
|
let rawUrl = extractURL(match[0]);
|
|
392
657
|
if (!rawUrl) return null;
|
|
393
658
|
let url = rawUrl;
|
|
394
|
-
if (!url.startsWith("http")) {
|
|
659
|
+
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
395
660
|
url = new URL(url, link.href).href;
|
|
396
661
|
}
|
|
397
|
-
|
|
398
|
-
url = encodeURI(url);
|
|
399
|
-
} catch (e) {
|
|
400
|
-
console.warn("[snapdom] Failed to encode font URL:", url);
|
|
401
|
-
}
|
|
402
|
-
if (ignoreIconFonts && isIconFont(url)) {
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
662
|
+
if (isIconFont(url)) return null;
|
|
405
663
|
if (resourceCache.has(url)) {
|
|
664
|
+
processedFontURLs.add(url);
|
|
406
665
|
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
|
|
407
666
|
}
|
|
667
|
+
if (processedFontURLs.has(url)) return null;
|
|
408
668
|
try {
|
|
409
669
|
const fontRes = await fetch(url);
|
|
410
670
|
const blob = await fontRes.blob();
|
|
@@ -414,18 +674,17 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
414
674
|
reader.readAsDataURL(blob);
|
|
415
675
|
});
|
|
416
676
|
resourceCache.set(url, b64);
|
|
677
|
+
processedFontURLs.add(url);
|
|
417
678
|
return { original: match[0], inlined: `url(${b64})` };
|
|
418
|
-
} catch (
|
|
419
|
-
console.warn("[snapdom] Failed to fetch font:", url);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
420
681
|
return null;
|
|
421
682
|
}
|
|
422
683
|
})
|
|
423
684
|
);
|
|
424
685
|
let cssFinal = cssText;
|
|
425
686
|
for (const r of inlinedCSS) {
|
|
426
|
-
if (r)
|
|
427
|
-
cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
428
|
-
}
|
|
687
|
+
if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
429
688
|
}
|
|
430
689
|
finalCSS += cssFinal + "\n";
|
|
431
690
|
} catch (e) {
|
|
@@ -438,24 +697,25 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
438
697
|
for (const rule of sheet.cssRules) {
|
|
439
698
|
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
440
699
|
const src = rule.style.getPropertyValue("src");
|
|
441
|
-
|
|
442
|
-
|
|
700
|
+
const family = rule.style.getPropertyValue("font-family");
|
|
701
|
+
if (!src || isIconFont(family)) continue;
|
|
702
|
+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
443
703
|
let inlinedSrc = src;
|
|
444
704
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
445
705
|
for (const match of matches) {
|
|
446
|
-
let rawUrl = match[
|
|
706
|
+
let rawUrl = match[2].trim();
|
|
447
707
|
if (!rawUrl) continue;
|
|
448
708
|
let url = rawUrl;
|
|
449
709
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
450
710
|
url = new URL(url, sheet.href || location.href).href;
|
|
451
711
|
}
|
|
452
|
-
if (
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
712
|
+
if (isIconFont(url)) continue;
|
|
455
713
|
if (resourceCache.has(url)) {
|
|
714
|
+
processedFontURLs.add(url);
|
|
456
715
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
457
716
|
continue;
|
|
458
717
|
}
|
|
718
|
+
if (processedFontURLs.has(url)) continue;
|
|
459
719
|
try {
|
|
460
720
|
const res = await fetch(url);
|
|
461
721
|
const blob = await res.blob();
|
|
@@ -465,13 +725,14 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
465
725
|
reader.readAsDataURL(blob);
|
|
466
726
|
});
|
|
467
727
|
resourceCache.set(url, b64);
|
|
728
|
+
processedFontURLs.add(url);
|
|
468
729
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
469
730
|
} catch (e) {
|
|
470
731
|
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
471
732
|
}
|
|
472
733
|
}
|
|
473
734
|
finalCSS += `@font-face {
|
|
474
|
-
font-family: ${
|
|
735
|
+
font-family: ${family};
|
|
475
736
|
src: ${inlinedSrc};
|
|
476
737
|
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
477
738
|
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
@@ -486,11 +747,13 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
486
747
|
}
|
|
487
748
|
for (const font of document.fonts) {
|
|
488
749
|
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
750
|
+
if (isIconFont(font.family)) continue;
|
|
489
751
|
let b64 = font._snapdomSrc;
|
|
490
752
|
if (!b64.startsWith("data:")) {
|
|
491
753
|
if (resourceCache.has(font._snapdomSrc)) {
|
|
492
754
|
b64 = resourceCache.get(font._snapdomSrc);
|
|
493
|
-
|
|
755
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
756
|
+
} else if (!processedFontURLs.has(font._snapdomSrc)) {
|
|
494
757
|
try {
|
|
495
758
|
const res = await fetch(font._snapdomSrc);
|
|
496
759
|
const blob = await res.blob();
|
|
@@ -500,6 +763,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
500
763
|
reader.readAsDataURL(blob);
|
|
501
764
|
});
|
|
502
765
|
resourceCache.set(font._snapdomSrc, b64);
|
|
766
|
+
processedFontURLs.add(font._snapdomSrc);
|
|
503
767
|
} catch (e) {
|
|
504
768
|
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
505
769
|
continue;
|
|
@@ -515,27 +779,56 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
515
779
|
`;
|
|
516
780
|
}
|
|
517
781
|
}
|
|
518
|
-
if (finalCSS
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
782
|
+
if (finalCSS) {
|
|
783
|
+
resourceCache.set("fonts-embed-css", finalCSS);
|
|
784
|
+
if (preCached) {
|
|
785
|
+
const style = document.createElement("style");
|
|
786
|
+
style.setAttribute("data-snapdom", "embedFonts");
|
|
787
|
+
style.textContent = finalCSS;
|
|
788
|
+
document.head.appendChild(style);
|
|
789
|
+
}
|
|
523
790
|
}
|
|
524
791
|
return finalCSS;
|
|
525
792
|
}
|
|
526
793
|
|
|
527
794
|
// src/modules/pseudo.js
|
|
528
|
-
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
|
|
795
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
|
|
529
796
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
530
|
-
for (const pseudo of ["::before", "::after"]) {
|
|
797
|
+
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
531
798
|
try {
|
|
532
799
|
const style = getStyle(source, pseudo);
|
|
533
|
-
if (!style) continue;
|
|
800
|
+
if (!style || typeof style[Symbol.iterator] !== "function") continue;
|
|
801
|
+
if (pseudo === "::first-letter") {
|
|
802
|
+
const normal = getComputedStyle(source);
|
|
803
|
+
const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
|
|
804
|
+
if (!isMeaningful) continue;
|
|
805
|
+
const textNode = Array.from(clone.childNodes).find(
|
|
806
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
|
|
807
|
+
);
|
|
808
|
+
if (!textNode) continue;
|
|
809
|
+
const text = textNode.textContent;
|
|
810
|
+
const match = text.match(/^([^\p{L}\p{N}\s]*[\p{L}\p{N}](?:['’])?)/u);
|
|
811
|
+
const first = match?.[0];
|
|
812
|
+
const rest = text.slice(first?.length || 0);
|
|
813
|
+
if (!first || /[\uD800-\uDFFF]/.test(first)) continue;
|
|
814
|
+
const span = document.createElement("span");
|
|
815
|
+
span.textContent = first;
|
|
816
|
+
span.dataset.snapdomPseudo = "::first-letter";
|
|
817
|
+
const snapshot = snapshotComputedStyle(style);
|
|
818
|
+
const key = getStyleKey(snapshot, "span", compress);
|
|
819
|
+
styleMap.set(span, key);
|
|
820
|
+
const restNode = document.createTextNode(rest);
|
|
821
|
+
clone.replaceChild(restNode, textNode);
|
|
822
|
+
clone.insertBefore(span, restNode);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
534
825
|
const content = style.getPropertyValue("content");
|
|
535
826
|
const bg = style.getPropertyValue("background-image");
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
827
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
828
|
+
const hasContent = content !== "none";
|
|
829
|
+
const hasBg = bg && bg !== "none";
|
|
830
|
+
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
831
|
+
if (hasContent || hasBg || hasBgColor) {
|
|
539
832
|
const fontFamily = style.getPropertyValue("font-family");
|
|
540
833
|
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
|
|
541
834
|
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
|
|
@@ -545,29 +838,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
545
838
|
const snapshot = snapshotComputedStyle(style);
|
|
546
839
|
const key = getStyleKey(snapshot, "span", compress);
|
|
547
840
|
styleMap.set(pseudoEl, key);
|
|
548
|
-
const isIconFont2 = fontFamily
|
|
549
|
-
|
|
550
|
-
)
|
|
551
|
-
let cleanContent = parseContent(content);
|
|
552
|
-
if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
|
|
841
|
+
const isIconFont2 = isIconFont(fontFamily);
|
|
842
|
+
const cleanContent = parseContent(content);
|
|
843
|
+
if (isIconFont2 && cleanContent.length === 1) {
|
|
553
844
|
const imgEl = document.createElement("img");
|
|
554
|
-
imgEl.src = await iconToImage(
|
|
555
|
-
|
|
556
|
-
fontFamily,
|
|
557
|
-
fontWeight,
|
|
558
|
-
fontSize,
|
|
559
|
-
color
|
|
560
|
-
);
|
|
561
|
-
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
845
|
+
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
846
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
562
847
|
pseudoEl.appendChild(imgEl);
|
|
563
848
|
} else if (cleanContent.startsWith("url(")) {
|
|
564
849
|
const rawUrl = extractURL(cleanContent);
|
|
565
|
-
if (rawUrl) {
|
|
850
|
+
if (rawUrl && rawUrl.trim() !== "") {
|
|
566
851
|
try {
|
|
567
852
|
const imgEl = document.createElement("img");
|
|
568
|
-
const dataUrl = await fetchImage(
|
|
853
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
|
|
569
854
|
imgEl.src = dataUrl;
|
|
570
|
-
imgEl.style =
|
|
855
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
571
856
|
pseudoEl.appendChild(imgEl);
|
|
572
857
|
} catch (e) {
|
|
573
858
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
@@ -576,24 +861,22 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
576
861
|
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
577
862
|
pseudoEl.textContent = cleanContent;
|
|
578
863
|
}
|
|
579
|
-
if (hasBg
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
864
|
+
if (hasBg) {
|
|
865
|
+
try {
|
|
866
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
867
|
+
const newBgParts = await Promise.all(
|
|
868
|
+
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
|
|
869
|
+
);
|
|
870
|
+
pseudoEl.style.backgroundImage = newBgParts.join(", ");
|
|
871
|
+
} catch (e) {
|
|
872
|
+
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
588
873
|
}
|
|
589
874
|
}
|
|
875
|
+
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
590
876
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
|
|
591
|
-
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
} else {
|
|
595
|
-
clone.appendChild(pseudoEl);
|
|
596
|
-
}
|
|
877
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor;
|
|
878
|
+
if (!hasVisibleBox) continue;
|
|
879
|
+
pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
|
|
597
880
|
}
|
|
598
881
|
} catch (e) {
|
|
599
882
|
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
@@ -608,28 +891,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
608
891
|
styleMap,
|
|
609
892
|
styleCache,
|
|
610
893
|
compress,
|
|
611
|
-
embedFonts
|
|
894
|
+
embedFonts,
|
|
895
|
+
useProxy
|
|
612
896
|
);
|
|
613
897
|
}
|
|
614
898
|
}
|
|
615
899
|
|
|
900
|
+
// src/modules/svgDefs.js
|
|
901
|
+
function inlineExternalDef(root) {
|
|
902
|
+
if (!root) return;
|
|
903
|
+
const defsSources = document.querySelectorAll("svg > defs");
|
|
904
|
+
if (!defsSources.length) return;
|
|
905
|
+
root.querySelectorAll("svg").forEach((svg) => {
|
|
906
|
+
const uses = svg.querySelectorAll("use");
|
|
907
|
+
if (!uses.length) return;
|
|
908
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
909
|
+
uses.forEach((use) => {
|
|
910
|
+
const href = use.getAttribute("xlink:href") || use.getAttribute("href");
|
|
911
|
+
if (href && href.startsWith("#")) usedIds.add(href.slice(1));
|
|
912
|
+
});
|
|
913
|
+
if (!usedIds.size) return;
|
|
914
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
915
|
+
for (const id of usedIds) {
|
|
916
|
+
for (const source of defsSources) {
|
|
917
|
+
const def = source.querySelector(`#${CSS.escape(id)}`);
|
|
918
|
+
if (def) {
|
|
919
|
+
defs.appendChild(def.cloneNode(true));
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (defs.childNodes.length) {
|
|
925
|
+
svg.insertBefore(defs, svg.firstChild);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
616
930
|
// src/core/prepare.js
|
|
617
|
-
async function prepareClone(element, compress = false, embedFonts = false) {
|
|
931
|
+
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
618
932
|
const styleMap = /* @__PURE__ */ new Map();
|
|
619
933
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
620
934
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
621
935
|
let clone;
|
|
622
936
|
try {
|
|
623
|
-
clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
|
|
937
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
|
|
624
938
|
} catch (e) {
|
|
625
939
|
console.warn("deepClone failed:", e);
|
|
626
940
|
throw e;
|
|
627
941
|
}
|
|
628
942
|
try {
|
|
629
|
-
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
|
|
943
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
|
|
630
944
|
} catch (e) {
|
|
631
945
|
console.warn("inlinePseudoElements failed:", e);
|
|
632
946
|
}
|
|
947
|
+
try {
|
|
948
|
+
inlineExternalDef(clone);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
console.warn("inlineExternalDef failed:", e);
|
|
951
|
+
}
|
|
633
952
|
let classCSS = "";
|
|
634
953
|
if (compress) {
|
|
635
954
|
const keyToClass = generateCSSClasses(styleMap);
|
|
@@ -667,6 +986,27 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
667
986
|
cloneNode.appendChild(inner);
|
|
668
987
|
}
|
|
669
988
|
}
|
|
989
|
+
if (element === nodeMap.get(clone)) {
|
|
990
|
+
const computed = styleCache.get(element) || window.getComputedStyle(element);
|
|
991
|
+
styleCache.set(element, computed);
|
|
992
|
+
const transform = stripTranslate(computed.transform);
|
|
993
|
+
clone.style.margin = "0";
|
|
994
|
+
clone.style.position = "static";
|
|
995
|
+
clone.style.top = "auto";
|
|
996
|
+
clone.style.left = "auto";
|
|
997
|
+
clone.style.right = "auto";
|
|
998
|
+
clone.style.bottom = "auto";
|
|
999
|
+
clone.style.zIndex = "auto";
|
|
1000
|
+
clone.style.float = "none";
|
|
1001
|
+
clone.style.clear = "none";
|
|
1002
|
+
clone.style.transform = transform || "";
|
|
1003
|
+
}
|
|
1004
|
+
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1005
|
+
if (originalNode.tagName === "PRE") {
|
|
1006
|
+
cloneNode.style.marginTop = "0";
|
|
1007
|
+
cloneNode.style.marginBlockStart = "0";
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
670
1010
|
return { clone, classCSS, styleCache };
|
|
671
1011
|
}
|
|
672
1012
|
|
|
@@ -676,8 +1016,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
676
1016
|
const processImg = async (img) => {
|
|
677
1017
|
const src = img.src;
|
|
678
1018
|
try {
|
|
679
|
-
const
|
|
680
|
-
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
1019
|
+
const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
|
|
681
1020
|
img.src = dataUrl;
|
|
682
1021
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
683
1022
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -697,33 +1036,34 @@ async function inlineImages(clone, options = {}) {
|
|
|
697
1036
|
// src/modules/background.js
|
|
698
1037
|
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
699
1038
|
const queue = [[source, clone]];
|
|
1039
|
+
const imageProps = [
|
|
1040
|
+
"background-image",
|
|
1041
|
+
"mask",
|
|
1042
|
+
"mask-image",
|
|
1043
|
+
"-webkit-mask-image",
|
|
1044
|
+
"mask-source",
|
|
1045
|
+
"mask-box-image-source",
|
|
1046
|
+
"mask-border-source",
|
|
1047
|
+
"-webkit-mask-box-image-source"
|
|
1048
|
+
];
|
|
700
1049
|
while (queue.length) {
|
|
701
1050
|
const [srcNode, cloneNode] = queue.shift();
|
|
702
1051
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
703
1052
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
dataUrl = bgCache.get(bgUrl);
|
|
714
|
-
} else {
|
|
715
|
-
const crossOrigin = options.crossOrigin ? options.crossOrigin(bgUrl) : "anonymous";
|
|
716
|
-
dataUrl = await fetchImage(bgUrl, 3e3, crossOrigin);
|
|
717
|
-
bgCache.set(bgUrl, dataUrl);
|
|
718
|
-
}
|
|
719
|
-
bgSplits[i] = `url(${dataUrl})`;
|
|
720
|
-
} catch {
|
|
721
|
-
bgSplits[i] = "none";
|
|
722
|
-
}
|
|
1053
|
+
for (const prop of imageProps) {
|
|
1054
|
+
const val = style.getPropertyValue(prop);
|
|
1055
|
+
if (!val || val === "none") continue;
|
|
1056
|
+
const splits = splitBackgroundImage(val);
|
|
1057
|
+
const inlined = await Promise.all(
|
|
1058
|
+
splits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
1059
|
+
);
|
|
1060
|
+
if (inlined.some((p) => p && p !== "none" && !/^url\(undefined/.test(p))) {
|
|
1061
|
+
cloneNode.style.setProperty(prop, inlined.join(", "));
|
|
723
1062
|
}
|
|
724
1063
|
}
|
|
725
|
-
|
|
726
|
-
|
|
1064
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
1065
|
+
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
1066
|
+
cloneNode.style.backgroundColor = bgColor;
|
|
727
1067
|
}
|
|
728
1068
|
const sChildren = Array.from(srcNode.children);
|
|
729
1069
|
const cChildren = Array.from(cloneNode.children);
|
|
@@ -736,13 +1076,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
736
1076
|
// src/core/capture.js
|
|
737
1077
|
async function captureDOM(element, options = {}) {
|
|
738
1078
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
739
|
-
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
1079
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
740
1080
|
let clone, classCSS, styleCache;
|
|
741
1081
|
let fontsCSS = "";
|
|
742
1082
|
let baseCSS = "";
|
|
743
1083
|
let dataURL;
|
|
744
1084
|
let svgString;
|
|
745
|
-
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
1085
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
|
|
746
1086
|
await new Promise((resolve) => {
|
|
747
1087
|
idle(async () => {
|
|
748
1088
|
await inlineImages(clone, options);
|
|
@@ -758,7 +1098,7 @@ async function captureDOM(element, options = {}) {
|
|
|
758
1098
|
if (embedFonts) {
|
|
759
1099
|
await new Promise((resolve) => {
|
|
760
1100
|
idle(async () => {
|
|
761
|
-
fontsCSS = await embedCustomFonts(
|
|
1101
|
+
fontsCSS = await embedCustomFonts();
|
|
762
1102
|
resolve();
|
|
763
1103
|
}, { fast });
|
|
764
1104
|
});
|
|
@@ -781,11 +1121,37 @@ async function captureDOM(element, options = {}) {
|
|
|
781
1121
|
await new Promise((resolve) => {
|
|
782
1122
|
idle(() => {
|
|
783
1123
|
const rect = element.getBoundingClientRect();
|
|
784
|
-
|
|
785
|
-
|
|
1124
|
+
let w = rect.width;
|
|
1125
|
+
let h = rect.height;
|
|
1126
|
+
const hasW = Number.isFinite(options.width);
|
|
1127
|
+
const hasH = Number.isFinite(options.height);
|
|
1128
|
+
const hasScale = typeof scale === "number" && scale !== 1;
|
|
1129
|
+
if (!hasScale) {
|
|
1130
|
+
const aspect = rect.width / rect.height;
|
|
1131
|
+
if (hasW && hasH) {
|
|
1132
|
+
w = options.width;
|
|
1133
|
+
h = options.height;
|
|
1134
|
+
} else if (hasW) {
|
|
1135
|
+
w = options.width;
|
|
1136
|
+
h = w / aspect;
|
|
1137
|
+
} else if (hasH) {
|
|
1138
|
+
h = options.height;
|
|
1139
|
+
w = h * aspect;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
w = Math.ceil(w);
|
|
1143
|
+
h = Math.ceil(h);
|
|
786
1144
|
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
787
1145
|
clone.style.transformOrigin = "top left";
|
|
788
|
-
if (
|
|
1146
|
+
if (!hasScale && (hasW || hasH)) {
|
|
1147
|
+
const originalW = rect.width;
|
|
1148
|
+
const originalH = rect.height;
|
|
1149
|
+
const scaleX = w / originalW;
|
|
1150
|
+
const scaleY = h / originalH;
|
|
1151
|
+
const existingTransform = clone.style.transform || "";
|
|
1152
|
+
const scaleTransform = `scale(${scaleX}, ${scaleY})`;
|
|
1153
|
+
clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
|
|
1154
|
+
} else if (hasScale && isSafari()) {
|
|
789
1155
|
clone.style.scale = `${scale}`;
|
|
790
1156
|
}
|
|
791
1157
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
@@ -840,9 +1206,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
840
1206
|
canvas.style.height = `${height}px`;
|
|
841
1207
|
return canvas;
|
|
842
1208
|
}
|
|
843
|
-
async function toBlob(url
|
|
844
|
-
|
|
845
|
-
|
|
1209
|
+
async function toBlob(url, {
|
|
1210
|
+
type = "svg",
|
|
1211
|
+
scale = 1,
|
|
1212
|
+
backgroundColor = "#fff",
|
|
1213
|
+
quality
|
|
1214
|
+
} = {}) {
|
|
1215
|
+
const mime = {
|
|
1216
|
+
jpg: "image/jpeg",
|
|
1217
|
+
jpeg: "image/jpeg",
|
|
1218
|
+
png: "image/png",
|
|
1219
|
+
webp: "image/webp"
|
|
1220
|
+
}[type] || "image/png";
|
|
1221
|
+
if (type === "svg") {
|
|
1222
|
+
const svgText = decodeURIComponent(url.split(",")[1]);
|
|
1223
|
+
return new Blob([svgText], { type: "image/svg+xml" });
|
|
1224
|
+
}
|
|
1225
|
+
const canvas = await createBackground(url, { dpr: 1, scale }, backgroundColor);
|
|
1226
|
+
return new Promise((resolve) => {
|
|
1227
|
+
canvas.toBlob((blob) => resolve(blob), `${mime}`, quality);
|
|
1228
|
+
});
|
|
846
1229
|
}
|
|
847
1230
|
async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
|
|
848
1231
|
const baseCanvas = await toCanvas(url, { dpr, scale });
|
|
@@ -894,6 +1277,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
|
|
|
894
1277
|
async function snapdom(element, options = {}) {
|
|
895
1278
|
options = { scale: 1, ...options };
|
|
896
1279
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1280
|
+
if (options.iconFonts) {
|
|
1281
|
+
extendIconFonts(options.iconFonts);
|
|
1282
|
+
}
|
|
897
1283
|
return await snapdom.capture(element, options);
|
|
898
1284
|
}
|
|
899
1285
|
snapdom.capture = async (el, options = {}) => {
|
|
@@ -906,7 +1292,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
906
1292
|
toRaw: () => url,
|
|
907
1293
|
toImg: () => toImg(url, { dpr, scale }),
|
|
908
1294
|
toCanvas: () => toCanvas(url, { dpr, scale }),
|
|
909
|
-
toBlob: () => toBlob(url),
|
|
1295
|
+
toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
|
|
910
1296
|
toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
|
|
911
1297
|
toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
|
|
912
1298
|
toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
|
|
@@ -916,7 +1302,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
916
1302
|
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
|
|
917
1303
|
snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
|
|
918
1304
|
snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
|
|
919
|
-
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
|
|
1305
|
+
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
|
|
920
1306
|
snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
|
|
921
1307
|
snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
|
|
922
1308
|
snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
|
|
@@ -933,13 +1319,12 @@ snapdom.download = async (el, options = {}) => {
|
|
|
933
1319
|
|
|
934
1320
|
// src/api/preCache.js
|
|
935
1321
|
async function preCache(root = document, options = {}) {
|
|
936
|
-
const { embedFonts = true, reset = false
|
|
1322
|
+
const { embedFonts = true, reset = false } = options;
|
|
937
1323
|
if (reset) {
|
|
938
1324
|
imageCache.clear();
|
|
939
1325
|
bgCache.clear();
|
|
940
1326
|
resourceCache.clear();
|
|
941
1327
|
baseCSSCache.clear();
|
|
942
|
-
computedStyleCache.clear();
|
|
943
1328
|
return;
|
|
944
1329
|
}
|
|
945
1330
|
await document.fonts.ready;
|
|
@@ -953,26 +1338,29 @@ async function preCache(root = document, options = {}) {
|
|
|
953
1338
|
for (const img of imgEls) {
|
|
954
1339
|
const src = img.src;
|
|
955
1340
|
if (!imageCache.has(src)) {
|
|
956
|
-
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
957
1341
|
promises.push(
|
|
958
|
-
fetchImage(src,
|
|
1342
|
+
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
959
1343
|
})
|
|
960
1344
|
);
|
|
961
1345
|
}
|
|
962
1346
|
}
|
|
963
1347
|
for (const el of allEls) {
|
|
964
|
-
const bg =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1348
|
+
const bg = getStyle(el).backgroundImage;
|
|
1349
|
+
if (bg && bg !== "none") {
|
|
1350
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
1351
|
+
for (const entry of bgSplits) {
|
|
1352
|
+
const isUrl = entry.startsWith("url(");
|
|
1353
|
+
if (isUrl) {
|
|
1354
|
+
promises.push(
|
|
1355
|
+
inlineSingleBackgroundEntry(entry, options).catch(() => {
|
|
1356
|
+
})
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
972
1360
|
}
|
|
973
1361
|
}
|
|
974
1362
|
if (embedFonts) {
|
|
975
|
-
await embedCustomFonts({
|
|
1363
|
+
await embedCustomFonts({ preCached: true });
|
|
976
1364
|
}
|
|
977
1365
|
await Promise.all(promises);
|
|
978
1366
|
}
|