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