@zumer/snapdom 1.8.0 → 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 +63 -30
- package/dist/snapdom.js +294 -85
- package/dist/snapdom.min.js +13 -13
- package/dist/snapdom.min.mjs +10 -10
- package/dist/snapdom.mjs +289 -82
- package/package.json +1 -1
- package/types/snapdom.d.ts +6 -2
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
**/
|
|
@@ -164,10 +164,9 @@ async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
|
164
164
|
if (bgCache.has(encodedUrl)) {
|
|
165
165
|
return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
|
|
166
166
|
} else {
|
|
167
|
-
const
|
|
168
|
-
const dataUrl = await fetchImage(encodedUrl, 3e3, crossOrigin);
|
|
167
|
+
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
169
168
|
bgCache.set(encodedUrl, dataUrl);
|
|
170
|
-
return options.skipInline ? void 0 : `url(${dataUrl})`;
|
|
169
|
+
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
if (isGradient || entry === "none") {
|
|
@@ -219,30 +218,84 @@ function extractURL(value) {
|
|
|
219
218
|
}
|
|
220
219
|
return url;
|
|
221
220
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
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}`);
|
|
237
266
|
if (imageCache.has(src)) {
|
|
267
|
+
console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
|
|
238
268
|
return Promise.resolve(imageCache.get(src));
|
|
239
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
|
+
}
|
|
240
292
|
return new Promise((resolve, reject) => {
|
|
241
293
|
const timeoutId = setTimeout(() => {
|
|
294
|
+
console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
|
|
242
295
|
reject(new Error("Image load timed out"));
|
|
243
296
|
}, timeout);
|
|
244
297
|
const image = new Image();
|
|
245
|
-
image.crossOrigin =
|
|
298
|
+
image.crossOrigin = crossOriginValue;
|
|
246
299
|
image.onload = async () => {
|
|
247
300
|
clearTimeout(timeoutId);
|
|
248
301
|
try {
|
|
@@ -252,20 +305,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
|
252
305
|
canvas.height = image.height;
|
|
253
306
|
const ctx = canvas.getContext("2d");
|
|
254
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 {
|
|
255
312
|
try {
|
|
256
|
-
const
|
|
257
|
-
imageCache.set(src,
|
|
258
|
-
resolve(
|
|
313
|
+
const fallbackDataURL = await fetchWithFallback(src);
|
|
314
|
+
imageCache.set(src, fallbackDataURL);
|
|
315
|
+
resolve(fallbackDataURL);
|
|
259
316
|
} catch (e) {
|
|
260
|
-
reject(
|
|
317
|
+
reject(e);
|
|
261
318
|
}
|
|
262
|
-
} catch (e) {
|
|
263
|
-
reject(e);
|
|
264
319
|
}
|
|
265
320
|
};
|
|
266
|
-
image.onerror = (
|
|
321
|
+
image.onerror = async () => {
|
|
267
322
|
clearTimeout(timeoutId);
|
|
268
|
-
|
|
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
|
+
}
|
|
269
331
|
};
|
|
270
332
|
image.src = src;
|
|
271
333
|
});
|
|
@@ -300,9 +362,9 @@ function stripTranslate(transform) {
|
|
|
300
362
|
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
301
363
|
}
|
|
302
364
|
function safeEncodeURI(uri) {
|
|
365
|
+
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
303
366
|
try {
|
|
304
|
-
|
|
305
|
-
return encodeURI(newURI);
|
|
367
|
+
return encodeURI(uri);
|
|
306
368
|
} catch {
|
|
307
369
|
return uri;
|
|
308
370
|
}
|
|
@@ -325,33 +387,91 @@ function splitBackgroundImage(bg) {
|
|
|
325
387
|
}
|
|
326
388
|
|
|
327
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
|
+
}
|
|
328
404
|
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
329
405
|
if (source.tagName === "STYLE") return;
|
|
330
406
|
if (!cache.has(source)) {
|
|
331
407
|
cache.set(source, getStyle(source));
|
|
332
408
|
}
|
|
333
409
|
const style = cache.get(source);
|
|
334
|
-
|
|
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
|
+
}
|
|
335
420
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
336
421
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
422
|
+
snapshotKeyCache.set(hash, key);
|
|
337
423
|
styleMap.set(clone, key);
|
|
338
424
|
}
|
|
339
425
|
|
|
340
426
|
// src/core/clone.js
|
|
341
|
-
function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
342
|
-
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") {
|
|
343
437
|
const spacer = document.createElement("div");
|
|
344
438
|
const rect = node.getBoundingClientRect();
|
|
345
439
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
346
440
|
return spacer;
|
|
347
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
|
+
}
|
|
348
468
|
if (node.tagName === "IFRAME") {
|
|
349
469
|
const fallback = document.createElement("div");
|
|
350
470
|
fallback.textContent = "";
|
|
351
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;`;
|
|
352
472
|
return fallback;
|
|
353
473
|
}
|
|
354
|
-
if (node.
|
|
474
|
+
if (node.getAttribute("data-capture") === "placeholder") {
|
|
355
475
|
const clone2 = node.cloneNode(false);
|
|
356
476
|
nodeMap.set(clone2, node);
|
|
357
477
|
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
@@ -372,14 +492,6 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
372
492
|
img.style.height = node.style.height || `${node.height}px`;
|
|
373
493
|
return img;
|
|
374
494
|
}
|
|
375
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
376
|
-
if (node.parentElement?.shadowRoot) {
|
|
377
|
-
const tag = node.parentElement.tagName.toLowerCase();
|
|
378
|
-
if (customElements.get(tag)) return null;
|
|
379
|
-
}
|
|
380
|
-
return node.cloneNode(true);
|
|
381
|
-
}
|
|
382
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
383
495
|
const clone = node.cloneNode(false);
|
|
384
496
|
nodeMap.set(clone, node);
|
|
385
497
|
if (node instanceof HTMLInputElement) {
|
|
@@ -404,13 +516,14 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
404
516
|
}
|
|
405
517
|
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
406
518
|
const frag = document.createDocumentFragment();
|
|
519
|
+
const rootElement = originalRoot || node;
|
|
407
520
|
node.childNodes.forEach((child) => {
|
|
408
|
-
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
|
|
521
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, rootElement);
|
|
409
522
|
if (clonedChild) frag.appendChild(clonedChild);
|
|
410
523
|
});
|
|
411
524
|
clone.appendChild(frag);
|
|
412
525
|
if (node.shadowRoot) {
|
|
413
|
-
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);
|
|
414
527
|
const shadowFrag = document.createDocumentFragment();
|
|
415
528
|
shadowContent.forEach((child) => shadowFrag.appendChild(child));
|
|
416
529
|
clone.appendChild(shadowFrag);
|
|
@@ -418,6 +531,43 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
418
531
|
return clone;
|
|
419
532
|
}
|
|
420
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
|
+
|
|
421
571
|
// src/modules/fonts.js
|
|
422
572
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
423
573
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
@@ -457,7 +607,7 @@ function injectLinkIfMissing(href) {
|
|
|
457
607
|
document.head.appendChild(link);
|
|
458
608
|
});
|
|
459
609
|
}
|
|
460
|
-
async function embedCustomFonts({
|
|
610
|
+
async function embedCustomFonts({ preCached = false } = {}) {
|
|
461
611
|
if (resourceCache.has("fonts-embed-css")) {
|
|
462
612
|
if (preCached) {
|
|
463
613
|
const style = document.createElement("style");
|
|
@@ -474,6 +624,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
474
624
|
const matches = Array.from(cssText.matchAll(importRegex));
|
|
475
625
|
for (const match of matches) {
|
|
476
626
|
const importUrl = match[1];
|
|
627
|
+
if (isIconFont(importUrl)) continue;
|
|
477
628
|
if (!isStylesheetLoaded(importUrl)) {
|
|
478
629
|
styleImports.push(importUrl);
|
|
479
630
|
}
|
|
@@ -486,7 +637,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
486
637
|
try {
|
|
487
638
|
const res = await fetch(link.href);
|
|
488
639
|
const cssText = await res.text();
|
|
489
|
-
if (
|
|
640
|
+
if (isIconFont(link.href) || isIconFont(cssText)) continue;
|
|
490
641
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
491
642
|
const inlinedCSS = await Promise.all(
|
|
492
643
|
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
|
|
@@ -496,7 +647,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
496
647
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
497
648
|
url = new URL(url, link.href).href;
|
|
498
649
|
}
|
|
499
|
-
if (
|
|
650
|
+
if (isIconFont(url)) return null;
|
|
500
651
|
if (resourceCache.has(url)) {
|
|
501
652
|
processedFontURLs.add(url);
|
|
502
653
|
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
|
|
@@ -534,7 +685,8 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
534
685
|
for (const rule of sheet.cssRules) {
|
|
535
686
|
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
536
687
|
const src = rule.style.getPropertyValue("src");
|
|
537
|
-
|
|
688
|
+
const family = rule.style.getPropertyValue("font-family");
|
|
689
|
+
if (!src || isIconFont(family)) continue;
|
|
538
690
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
539
691
|
let inlinedSrc = src;
|
|
540
692
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
@@ -545,7 +697,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
545
697
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
546
698
|
url = new URL(url, sheet.href || location.href).href;
|
|
547
699
|
}
|
|
548
|
-
if (
|
|
700
|
+
if (isIconFont(url)) continue;
|
|
549
701
|
if (resourceCache.has(url)) {
|
|
550
702
|
processedFontURLs.add(url);
|
|
551
703
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
@@ -568,7 +720,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
568
720
|
}
|
|
569
721
|
}
|
|
570
722
|
finalCSS += `@font-face {
|
|
571
|
-
font-family: ${
|
|
723
|
+
font-family: ${family};
|
|
572
724
|
src: ${inlinedSrc};
|
|
573
725
|
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
574
726
|
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
@@ -583,6 +735,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
583
735
|
}
|
|
584
736
|
for (const font of document.fonts) {
|
|
585
737
|
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
738
|
+
if (isIconFont(font.family)) continue;
|
|
586
739
|
let b64 = font._snapdomSrc;
|
|
587
740
|
if (!b64.startsWith("data:")) {
|
|
588
741
|
if (resourceCache.has(font._snapdomSrc)) {
|
|
@@ -627,7 +780,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
627
780
|
}
|
|
628
781
|
|
|
629
782
|
// src/modules/pseudo.js
|
|
630
|
-
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
|
|
783
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
|
|
631
784
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
632
785
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
633
786
|
try {
|
|
@@ -673,23 +826,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
673
826
|
const snapshot = snapshotComputedStyle(style);
|
|
674
827
|
const key = getStyleKey(snapshot, "span", compress);
|
|
675
828
|
styleMap.set(pseudoEl, key);
|
|
676
|
-
const isIconFont2 = fontFamily
|
|
677
|
-
fontFamily
|
|
678
|
-
);
|
|
829
|
+
const isIconFont2 = isIconFont(fontFamily);
|
|
679
830
|
const cleanContent = parseContent(content);
|
|
680
|
-
if (
|
|
831
|
+
if (isIconFont2 && cleanContent.length === 1) {
|
|
681
832
|
const imgEl = document.createElement("img");
|
|
682
833
|
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
683
|
-
imgEl.style =
|
|
834
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
684
835
|
pseudoEl.appendChild(imgEl);
|
|
685
836
|
} else if (cleanContent.startsWith("url(")) {
|
|
686
837
|
const rawUrl = extractURL(cleanContent);
|
|
687
838
|
if (rawUrl && rawUrl.trim() !== "") {
|
|
688
839
|
try {
|
|
689
840
|
const imgEl = document.createElement("img");
|
|
690
|
-
const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
|
|
841
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
|
|
691
842
|
imgEl.src = dataUrl;
|
|
692
|
-
imgEl.style =
|
|
843
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
693
844
|
pseudoEl.appendChild(imgEl);
|
|
694
845
|
} catch (e) {
|
|
695
846
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
@@ -728,28 +879,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
728
879
|
styleMap,
|
|
729
880
|
styleCache,
|
|
730
881
|
compress,
|
|
731
|
-
embedFonts
|
|
882
|
+
embedFonts,
|
|
883
|
+
useProxy
|
|
732
884
|
);
|
|
733
885
|
}
|
|
734
886
|
}
|
|
735
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
|
+
|
|
736
918
|
// src/core/prepare.js
|
|
737
|
-
async function prepareClone(element, compress = false, embedFonts = false) {
|
|
919
|
+
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
738
920
|
const styleMap = /* @__PURE__ */ new Map();
|
|
739
921
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
740
922
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
741
923
|
let clone;
|
|
742
924
|
try {
|
|
743
|
-
clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
|
|
925
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
|
|
744
926
|
} catch (e) {
|
|
745
927
|
console.warn("deepClone failed:", e);
|
|
746
928
|
throw e;
|
|
747
929
|
}
|
|
748
930
|
try {
|
|
749
|
-
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
|
|
931
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
|
|
750
932
|
} catch (e) {
|
|
751
933
|
console.warn("inlinePseudoElements failed:", e);
|
|
752
934
|
}
|
|
935
|
+
try {
|
|
936
|
+
inlineExternalDef(clone);
|
|
937
|
+
} catch (e) {
|
|
938
|
+
console.warn("inlineExternalDef failed:", e);
|
|
939
|
+
}
|
|
753
940
|
let classCSS = "";
|
|
754
941
|
if (compress) {
|
|
755
942
|
const keyToClass = generateCSSClasses(styleMap);
|
|
@@ -802,6 +989,12 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
802
989
|
clone.style.clear = "none";
|
|
803
990
|
clone.style.transform = transform || "";
|
|
804
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
|
+
}
|
|
805
998
|
return { clone, classCSS, styleCache };
|
|
806
999
|
}
|
|
807
1000
|
|
|
@@ -811,8 +1004,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
811
1004
|
const processImg = async (img) => {
|
|
812
1005
|
const src = img.src;
|
|
813
1006
|
try {
|
|
814
|
-
const
|
|
815
|
-
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
1007
|
+
const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
|
|
816
1008
|
img.src = dataUrl;
|
|
817
1009
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
818
1010
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -850,10 +1042,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
850
1042
|
const newBgParts = await Promise.all(
|
|
851
1043
|
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
852
1044
|
);
|
|
853
|
-
|
|
854
|
-
(p) => p && p !== "none" && !/^url\(undefined\)/.test(p)
|
|
855
|
-
);
|
|
856
|
-
if (hasRealBg) {
|
|
1045
|
+
if (newBgParts.some((p) => p && p !== "none" && !/^url\(undefined\)/.test(p))) {
|
|
857
1046
|
cloneNode.style.backgroundImage = newBgParts.join(", ");
|
|
858
1047
|
}
|
|
859
1048
|
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
@@ -870,14 +1059,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
870
1059
|
// src/core/capture.js
|
|
871
1060
|
async function captureDOM(element, options = {}) {
|
|
872
1061
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
873
|
-
|
|
874
|
-
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
1062
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
875
1063
|
let clone, classCSS, styleCache;
|
|
876
1064
|
let fontsCSS = "";
|
|
877
1065
|
let baseCSS = "";
|
|
878
1066
|
let dataURL;
|
|
879
1067
|
let svgString;
|
|
880
|
-
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
1068
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
|
|
881
1069
|
await new Promise((resolve) => {
|
|
882
1070
|
idle(async () => {
|
|
883
1071
|
await inlineImages(clone, options);
|
|
@@ -893,7 +1081,7 @@ async function captureDOM(element, options = {}) {
|
|
|
893
1081
|
if (embedFonts) {
|
|
894
1082
|
await new Promise((resolve) => {
|
|
895
1083
|
idle(async () => {
|
|
896
|
-
fontsCSS = await embedCustomFonts(
|
|
1084
|
+
fontsCSS = await embedCustomFonts();
|
|
897
1085
|
resolve();
|
|
898
1086
|
}, { fast });
|
|
899
1087
|
});
|
|
@@ -1001,9 +1189,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1001
1189
|
canvas.style.height = `${height}px`;
|
|
1002
1190
|
return canvas;
|
|
1003
1191
|
}
|
|
1004
|
-
async function toBlob(url
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
});
|
|
1007
1212
|
}
|
|
1008
1213
|
async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
|
|
1009
1214
|
const baseCanvas = await toCanvas(url, { dpr, scale });
|
|
@@ -1055,6 +1260,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
|
|
|
1055
1260
|
async function snapdom(element, options = {}) {
|
|
1056
1261
|
options = { scale: 1, ...options };
|
|
1057
1262
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1263
|
+
if (options.iconFonts) {
|
|
1264
|
+
extendIconFonts(options.iconFonts);
|
|
1265
|
+
}
|
|
1058
1266
|
return await snapdom.capture(element, options);
|
|
1059
1267
|
}
|
|
1060
1268
|
snapdom.capture = async (el, options = {}) => {
|
|
@@ -1067,7 +1275,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
1067
1275
|
toRaw: () => url,
|
|
1068
1276
|
toImg: () => toImg(url, { dpr, scale }),
|
|
1069
1277
|
toCanvas: () => toCanvas(url, { dpr, scale }),
|
|
1070
|
-
toBlob: () => toBlob(url),
|
|
1278
|
+
toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
|
|
1071
1279
|
toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
|
|
1072
1280
|
toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
|
|
1073
1281
|
toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
|
|
@@ -1077,7 +1285,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
1077
1285
|
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
|
|
1078
1286
|
snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
|
|
1079
1287
|
snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
|
|
1080
|
-
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
|
|
1288
|
+
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
|
|
1081
1289
|
snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
|
|
1082
1290
|
snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
|
|
1083
1291
|
snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
|
|
@@ -1094,7 +1302,7 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1094
1302
|
|
|
1095
1303
|
// src/api/preCache.js
|
|
1096
1304
|
async function preCache(root = document, options = {}) {
|
|
1097
|
-
const { embedFonts = true, reset = false
|
|
1305
|
+
const { embedFonts = true, reset = false } = options;
|
|
1098
1306
|
if (reset) {
|
|
1099
1307
|
imageCache.clear();
|
|
1100
1308
|
bgCache.clear();
|
|
@@ -1113,9 +1321,8 @@ async function preCache(root = document, options = {}) {
|
|
|
1113
1321
|
for (const img of imgEls) {
|
|
1114
1322
|
const src = img.src;
|
|
1115
1323
|
if (!imageCache.has(src)) {
|
|
1116
|
-
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
1117
1324
|
promises.push(
|
|
1118
|
-
fetchImage(src,
|
|
1325
|
+
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
1119
1326
|
})
|
|
1120
1327
|
);
|
|
1121
1328
|
}
|
|
@@ -1128,7 +1335,7 @@ async function preCache(root = document, options = {}) {
|
|
|
1128
1335
|
const isUrl = entry.startsWith("url(");
|
|
1129
1336
|
if (isUrl) {
|
|
1130
1337
|
promises.push(
|
|
1131
|
-
inlineSingleBackgroundEntry(entry,
|
|
1338
|
+
inlineSingleBackgroundEntry(entry, options).catch(() => {
|
|
1132
1339
|
})
|
|
1133
1340
|
);
|
|
1134
1341
|
}
|
|
@@ -1136,7 +1343,7 @@ async function preCache(root = document, options = {}) {
|
|
|
1136
1343
|
}
|
|
1137
1344
|
}
|
|
1138
1345
|
if (embedFonts) {
|
|
1139
|
-
await embedCustomFonts({
|
|
1346
|
+
await embedCustomFonts({ preCached: true });
|
|
1140
1347
|
}
|
|
1141
1348
|
await Promise.all(promises);
|
|
1142
1349
|
}
|