@zumer/snapdom 1.8.0 → 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 +73 -34
- package/dist/snapdom.js +340 -114
- package/dist/snapdom.min.js +13 -13
- package/dist/snapdom.min.mjs +12 -12
- package/dist/snapdom.mjs +334 -110
- package/package.json +1 -1
- package/types/snapdom.d.ts +7 -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-next-dev.0
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -155,19 +155,17 @@ function generateCSSClasses(styleMap) {
|
|
|
155
155
|
|
|
156
156
|
// src/utils/helpers.js
|
|
157
157
|
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
158
|
-
const
|
|
158
|
+
const match = entry.match(/url\(["']?(.*?)["']?\)/);
|
|
159
|
+
const rawUrl = match?.[1];
|
|
159
160
|
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
160
|
-
if (
|
|
161
|
-
const rawUrl = extractURL(entry);
|
|
162
|
-
if (!rawUrl) return entry;
|
|
161
|
+
if (rawUrl) {
|
|
163
162
|
const encodedUrl = safeEncodeURI(rawUrl);
|
|
164
163
|
if (bgCache.has(encodedUrl)) {
|
|
165
164
|
return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
|
|
166
165
|
} else {
|
|
167
|
-
const
|
|
168
|
-
const dataUrl = await fetchImage(encodedUrl, 3e3, crossOrigin);
|
|
166
|
+
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
169
167
|
bgCache.set(encodedUrl, dataUrl);
|
|
170
|
-
return options.skipInline ? void 0 : `url(${dataUrl})`;
|
|
168
|
+
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
171
|
if (isGradient || entry === "none") {
|
|
@@ -219,30 +217,84 @@ function extractURL(value) {
|
|
|
219
217
|
}
|
|
220
218
|
return url;
|
|
221
219
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
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}`);
|
|
237
265
|
if (imageCache.has(src)) {
|
|
266
|
+
console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
|
|
238
267
|
return Promise.resolve(imageCache.get(src));
|
|
239
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
|
+
}
|
|
240
291
|
return new Promise((resolve, reject) => {
|
|
241
292
|
const timeoutId = setTimeout(() => {
|
|
293
|
+
console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
|
|
242
294
|
reject(new Error("Image load timed out"));
|
|
243
295
|
}, timeout);
|
|
244
296
|
const image = new Image();
|
|
245
|
-
image.crossOrigin =
|
|
297
|
+
image.crossOrigin = crossOriginValue;
|
|
246
298
|
image.onload = async () => {
|
|
247
299
|
clearTimeout(timeoutId);
|
|
248
300
|
try {
|
|
@@ -252,20 +304,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
|
|
|
252
304
|
canvas.height = image.height;
|
|
253
305
|
const ctx = canvas.getContext("2d");
|
|
254
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 {
|
|
255
311
|
try {
|
|
256
|
-
const
|
|
257
|
-
imageCache.set(src,
|
|
258
|
-
resolve(
|
|
312
|
+
const fallbackDataURL = await fetchWithFallback(src);
|
|
313
|
+
imageCache.set(src, fallbackDataURL);
|
|
314
|
+
resolve(fallbackDataURL);
|
|
259
315
|
} catch (e) {
|
|
260
|
-
reject(
|
|
316
|
+
reject(e);
|
|
261
317
|
}
|
|
262
|
-
} catch (e) {
|
|
263
|
-
reject(e);
|
|
264
318
|
}
|
|
265
319
|
};
|
|
266
|
-
image.onerror = (
|
|
320
|
+
image.onerror = async () => {
|
|
267
321
|
clearTimeout(timeoutId);
|
|
268
|
-
|
|
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
|
+
}
|
|
269
330
|
};
|
|
270
331
|
image.src = src;
|
|
271
332
|
});
|
|
@@ -300,9 +361,9 @@ function stripTranslate(transform) {
|
|
|
300
361
|
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
301
362
|
}
|
|
302
363
|
function safeEncodeURI(uri) {
|
|
364
|
+
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
303
365
|
try {
|
|
304
|
-
|
|
305
|
-
return encodeURI(newURI);
|
|
366
|
+
return encodeURI(uri);
|
|
306
367
|
} catch {
|
|
307
368
|
return uri;
|
|
308
369
|
}
|
|
@@ -325,33 +386,90 @@ function splitBackgroundImage(bg) {
|
|
|
325
386
|
}
|
|
326
387
|
|
|
327
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
|
+
}
|
|
328
403
|
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
329
404
|
if (source.tagName === "STYLE") return;
|
|
330
405
|
if (!cache.has(source)) {
|
|
331
406
|
cache.set(source, getStyle(source));
|
|
332
407
|
}
|
|
333
408
|
const style = cache.get(source);
|
|
334
|
-
|
|
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
|
+
}
|
|
335
419
|
const tagName = source.tagName?.toLowerCase() || "div";
|
|
336
420
|
const key = getStyleKey(snapshot, tagName, compress);
|
|
421
|
+
snapshotKeyCache.set(hash, key);
|
|
337
422
|
styleMap.set(clone, key);
|
|
338
423
|
}
|
|
339
424
|
|
|
340
425
|
// src/core/clone.js
|
|
341
|
-
function
|
|
342
|
-
|
|
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") {
|
|
343
435
|
const spacer = document.createElement("div");
|
|
344
436
|
const rect = node.getBoundingClientRect();
|
|
345
437
|
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
|
|
346
438
|
return spacer;
|
|
347
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
|
+
}
|
|
348
466
|
if (node.tagName === "IFRAME") {
|
|
349
467
|
const fallback = document.createElement("div");
|
|
350
468
|
fallback.textContent = "";
|
|
351
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;`;
|
|
352
470
|
return fallback;
|
|
353
471
|
}
|
|
354
|
-
if (node.
|
|
472
|
+
if (node.getAttribute("data-capture") === "placeholder") {
|
|
355
473
|
const clone2 = node.cloneNode(false);
|
|
356
474
|
nodeMap.set(clone2, node);
|
|
357
475
|
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
@@ -372,14 +490,6 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
372
490
|
img.style.height = node.style.height || `${node.height}px`;
|
|
373
491
|
return img;
|
|
374
492
|
}
|
|
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
493
|
const clone = node.cloneNode(false);
|
|
384
494
|
nodeMap.set(clone, node);
|
|
385
495
|
if (node instanceof HTMLInputElement) {
|
|
@@ -390,8 +500,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
390
500
|
if (node.checked) clone.setAttribute("checked", "");
|
|
391
501
|
}
|
|
392
502
|
} else if (node instanceof HTMLTextAreaElement) {
|
|
393
|
-
|
|
503
|
+
const rect = node.getBoundingClientRect();
|
|
394
504
|
clone.textContent = node.value;
|
|
505
|
+
clone.style.width = `${rect.width}px`;
|
|
506
|
+
clone.style.height = `${rect.height}px`;
|
|
395
507
|
} else if (node instanceof HTMLSelectElement) {
|
|
396
508
|
clone.value = node.value;
|
|
397
509
|
Array.from(clone.options).forEach((opt) => {
|
|
@@ -403,21 +515,71 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
|
403
515
|
});
|
|
404
516
|
}
|
|
405
517
|
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
}
|
|
417
542
|
}
|
|
418
543
|
return clone;
|
|
419
544
|
}
|
|
420
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
|
+
|
|
421
583
|
// src/modules/fonts.js
|
|
422
584
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
423
585
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
@@ -457,7 +619,7 @@ function injectLinkIfMissing(href) {
|
|
|
457
619
|
document.head.appendChild(link);
|
|
458
620
|
});
|
|
459
621
|
}
|
|
460
|
-
async function embedCustomFonts({
|
|
622
|
+
async function embedCustomFonts({ preCached = false } = {}) {
|
|
461
623
|
if (resourceCache.has("fonts-embed-css")) {
|
|
462
624
|
if (preCached) {
|
|
463
625
|
const style = document.createElement("style");
|
|
@@ -474,6 +636,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
474
636
|
const matches = Array.from(cssText.matchAll(importRegex));
|
|
475
637
|
for (const match of matches) {
|
|
476
638
|
const importUrl = match[1];
|
|
639
|
+
if (isIconFont(importUrl)) continue;
|
|
477
640
|
if (!isStylesheetLoaded(importUrl)) {
|
|
478
641
|
styleImports.push(importUrl);
|
|
479
642
|
}
|
|
@@ -486,7 +649,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
486
649
|
try {
|
|
487
650
|
const res = await fetch(link.href);
|
|
488
651
|
const cssText = await res.text();
|
|
489
|
-
if (
|
|
652
|
+
if (isIconFont(link.href) || isIconFont(cssText)) continue;
|
|
490
653
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
491
654
|
const inlinedCSS = await Promise.all(
|
|
492
655
|
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
|
|
@@ -496,7 +659,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
496
659
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
497
660
|
url = new URL(url, link.href).href;
|
|
498
661
|
}
|
|
499
|
-
if (
|
|
662
|
+
if (isIconFont(url)) return null;
|
|
500
663
|
if (resourceCache.has(url)) {
|
|
501
664
|
processedFontURLs.add(url);
|
|
502
665
|
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
|
|
@@ -534,7 +697,8 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
534
697
|
for (const rule of sheet.cssRules) {
|
|
535
698
|
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
536
699
|
const src = rule.style.getPropertyValue("src");
|
|
537
|
-
|
|
700
|
+
const family = rule.style.getPropertyValue("font-family");
|
|
701
|
+
if (!src || isIconFont(family)) continue;
|
|
538
702
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
539
703
|
let inlinedSrc = src;
|
|
540
704
|
const matches = Array.from(src.matchAll(urlRegex));
|
|
@@ -545,7 +709,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
545
709
|
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
546
710
|
url = new URL(url, sheet.href || location.href).href;
|
|
547
711
|
}
|
|
548
|
-
if (
|
|
712
|
+
if (isIconFont(url)) continue;
|
|
549
713
|
if (resourceCache.has(url)) {
|
|
550
714
|
processedFontURLs.add(url);
|
|
551
715
|
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
|
|
@@ -568,7 +732,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
568
732
|
}
|
|
569
733
|
}
|
|
570
734
|
finalCSS += `@font-face {
|
|
571
|
-
font-family: ${
|
|
735
|
+
font-family: ${family};
|
|
572
736
|
src: ${inlinedSrc};
|
|
573
737
|
font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
|
|
574
738
|
font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
|
|
@@ -583,6 +747,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
583
747
|
}
|
|
584
748
|
for (const font of document.fonts) {
|
|
585
749
|
if (font.family && font.status === "loaded" && font._snapdomSrc) {
|
|
750
|
+
if (isIconFont(font.family)) continue;
|
|
586
751
|
let b64 = font._snapdomSrc;
|
|
587
752
|
if (!b64.startsWith("data:")) {
|
|
588
753
|
if (resourceCache.has(font._snapdomSrc)) {
|
|
@@ -627,7 +792,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
|
|
|
627
792
|
}
|
|
628
793
|
|
|
629
794
|
// src/modules/pseudo.js
|
|
630
|
-
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
|
|
795
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
|
|
631
796
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
632
797
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
633
798
|
try {
|
|
@@ -673,23 +838,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
673
838
|
const snapshot = snapshotComputedStyle(style);
|
|
674
839
|
const key = getStyleKey(snapshot, "span", compress);
|
|
675
840
|
styleMap.set(pseudoEl, key);
|
|
676
|
-
const isIconFont2 = fontFamily
|
|
677
|
-
fontFamily
|
|
678
|
-
);
|
|
841
|
+
const isIconFont2 = isIconFont(fontFamily);
|
|
679
842
|
const cleanContent = parseContent(content);
|
|
680
|
-
if (
|
|
843
|
+
if (isIconFont2 && cleanContent.length === 1) {
|
|
681
844
|
const imgEl = document.createElement("img");
|
|
682
845
|
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
683
|
-
imgEl.style =
|
|
846
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
684
847
|
pseudoEl.appendChild(imgEl);
|
|
685
848
|
} else if (cleanContent.startsWith("url(")) {
|
|
686
849
|
const rawUrl = extractURL(cleanContent);
|
|
687
850
|
if (rawUrl && rawUrl.trim() !== "") {
|
|
688
851
|
try {
|
|
689
852
|
const imgEl = document.createElement("img");
|
|
690
|
-
const dataUrl = await fetchImage(safeEncodeURI(rawUrl));
|
|
853
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
|
|
691
854
|
imgEl.src = dataUrl;
|
|
692
|
-
imgEl.style =
|
|
855
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
693
856
|
pseudoEl.appendChild(imgEl);
|
|
694
857
|
} catch (e) {
|
|
695
858
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
@@ -728,28 +891,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
|
|
|
728
891
|
styleMap,
|
|
729
892
|
styleCache,
|
|
730
893
|
compress,
|
|
731
|
-
embedFonts
|
|
894
|
+
embedFonts,
|
|
895
|
+
useProxy
|
|
732
896
|
);
|
|
733
897
|
}
|
|
734
898
|
}
|
|
735
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
|
+
|
|
736
930
|
// src/core/prepare.js
|
|
737
|
-
async function prepareClone(element, compress = false, embedFonts = false) {
|
|
931
|
+
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
738
932
|
const styleMap = /* @__PURE__ */ new Map();
|
|
739
933
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
740
934
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
741
935
|
let clone;
|
|
742
936
|
try {
|
|
743
|
-
clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
|
|
937
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
|
|
744
938
|
} catch (e) {
|
|
745
939
|
console.warn("deepClone failed:", e);
|
|
746
940
|
throw e;
|
|
747
941
|
}
|
|
748
942
|
try {
|
|
749
|
-
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
|
|
943
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
|
|
750
944
|
} catch (e) {
|
|
751
945
|
console.warn("inlinePseudoElements failed:", e);
|
|
752
946
|
}
|
|
947
|
+
try {
|
|
948
|
+
inlineExternalDef(clone);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
console.warn("inlineExternalDef failed:", e);
|
|
951
|
+
}
|
|
753
952
|
let classCSS = "";
|
|
754
953
|
if (compress) {
|
|
755
954
|
const keyToClass = generateCSSClasses(styleMap);
|
|
@@ -802,6 +1001,12 @@ async function prepareClone(element, compress = false, embedFonts = false) {
|
|
|
802
1001
|
clone.style.clear = "none";
|
|
803
1002
|
clone.style.transform = transform || "";
|
|
804
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
|
+
}
|
|
805
1010
|
return { clone, classCSS, styleCache };
|
|
806
1011
|
}
|
|
807
1012
|
|
|
@@ -811,8 +1016,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
811
1016
|
const processImg = async (img) => {
|
|
812
1017
|
const src = img.src;
|
|
813
1018
|
try {
|
|
814
|
-
const
|
|
815
|
-
const dataUrl = await fetchImage(src, 3e3, crossOrigin);
|
|
1019
|
+
const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
|
|
816
1020
|
img.src = dataUrl;
|
|
817
1021
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
818
1022
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
@@ -832,30 +1036,32 @@ async function inlineImages(clone, options = {}) {
|
|
|
832
1036
|
// src/modules/background.js
|
|
833
1037
|
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
834
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
|
+
];
|
|
835
1049
|
while (queue.length) {
|
|
836
1050
|
const [srcNode, cloneNode] = queue.shift();
|
|
837
1051
|
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
838
1052
|
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
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(", "));
|
|
846
1062
|
}
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
const bgSplits = splitBackgroundImage(bg);
|
|
850
|
-
const newBgParts = await Promise.all(
|
|
851
|
-
bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
|
|
852
|
-
);
|
|
853
|
-
const hasRealBg = newBgParts.some(
|
|
854
|
-
(p) => p && p !== "none" && !/^url\(undefined\)/.test(p)
|
|
855
|
-
);
|
|
856
|
-
if (hasRealBg) {
|
|
857
|
-
cloneNode.style.backgroundImage = newBgParts.join(", ");
|
|
858
1063
|
}
|
|
1064
|
+
const bgColor = style.getPropertyValue("background-color");
|
|
859
1065
|
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
860
1066
|
cloneNode.style.backgroundColor = bgColor;
|
|
861
1067
|
}
|
|
@@ -870,14 +1076,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
870
1076
|
// src/core/capture.js
|
|
871
1077
|
async function captureDOM(element, options = {}) {
|
|
872
1078
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
873
|
-
|
|
874
|
-
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
1079
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
|
|
875
1080
|
let clone, classCSS, styleCache;
|
|
876
1081
|
let fontsCSS = "";
|
|
877
1082
|
let baseCSS = "";
|
|
878
1083
|
let dataURL;
|
|
879
1084
|
let svgString;
|
|
880
|
-
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
|
|
1085
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
|
|
881
1086
|
await new Promise((resolve) => {
|
|
882
1087
|
idle(async () => {
|
|
883
1088
|
await inlineImages(clone, options);
|
|
@@ -893,7 +1098,7 @@ async function captureDOM(element, options = {}) {
|
|
|
893
1098
|
if (embedFonts) {
|
|
894
1099
|
await new Promise((resolve) => {
|
|
895
1100
|
idle(async () => {
|
|
896
|
-
fontsCSS = await embedCustomFonts(
|
|
1101
|
+
fontsCSS = await embedCustomFonts();
|
|
897
1102
|
resolve();
|
|
898
1103
|
}, { fast });
|
|
899
1104
|
});
|
|
@@ -1001,9 +1206,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1001
1206
|
canvas.style.height = `${height}px`;
|
|
1002
1207
|
return canvas;
|
|
1003
1208
|
}
|
|
1004
|
-
async function toBlob(url
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
});
|
|
1007
1229
|
}
|
|
1008
1230
|
async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
|
|
1009
1231
|
const baseCanvas = await toCanvas(url, { dpr, scale });
|
|
@@ -1055,6 +1277,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
|
|
|
1055
1277
|
async function snapdom(element, options = {}) {
|
|
1056
1278
|
options = { scale: 1, ...options };
|
|
1057
1279
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1280
|
+
if (options.iconFonts) {
|
|
1281
|
+
extendIconFonts(options.iconFonts);
|
|
1282
|
+
}
|
|
1058
1283
|
return await snapdom.capture(element, options);
|
|
1059
1284
|
}
|
|
1060
1285
|
snapdom.capture = async (el, options = {}) => {
|
|
@@ -1067,7 +1292,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
1067
1292
|
toRaw: () => url,
|
|
1068
1293
|
toImg: () => toImg(url, { dpr, scale }),
|
|
1069
1294
|
toCanvas: () => toCanvas(url, { dpr, scale }),
|
|
1070
|
-
toBlob: () => toBlob(url),
|
|
1295
|
+
toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
|
|
1071
1296
|
toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
|
|
1072
1297
|
toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
|
|
1073
1298
|
toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
|
|
@@ -1077,7 +1302,7 @@ snapdom.capture = async (el, options = {}) => {
|
|
|
1077
1302
|
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
|
|
1078
1303
|
snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
|
|
1079
1304
|
snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
|
|
1080
|
-
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
|
|
1305
|
+
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
|
|
1081
1306
|
snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
|
|
1082
1307
|
snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
|
|
1083
1308
|
snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
|
|
@@ -1094,7 +1319,7 @@ snapdom.download = async (el, options = {}) => {
|
|
|
1094
1319
|
|
|
1095
1320
|
// src/api/preCache.js
|
|
1096
1321
|
async function preCache(root = document, options = {}) {
|
|
1097
|
-
const { embedFonts = true, reset = false
|
|
1322
|
+
const { embedFonts = true, reset = false } = options;
|
|
1098
1323
|
if (reset) {
|
|
1099
1324
|
imageCache.clear();
|
|
1100
1325
|
bgCache.clear();
|
|
@@ -1113,9 +1338,8 @@ async function preCache(root = document, options = {}) {
|
|
|
1113
1338
|
for (const img of imgEls) {
|
|
1114
1339
|
const src = img.src;
|
|
1115
1340
|
if (!imageCache.has(src)) {
|
|
1116
|
-
const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
|
|
1117
1341
|
promises.push(
|
|
1118
|
-
fetchImage(src,
|
|
1342
|
+
fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
1119
1343
|
})
|
|
1120
1344
|
);
|
|
1121
1345
|
}
|
|
@@ -1128,7 +1352,7 @@ async function preCache(root = document, options = {}) {
|
|
|
1128
1352
|
const isUrl = entry.startsWith("url(");
|
|
1129
1353
|
if (isUrl) {
|
|
1130
1354
|
promises.push(
|
|
1131
|
-
inlineSingleBackgroundEntry(entry,
|
|
1355
|
+
inlineSingleBackgroundEntry(entry, options).catch(() => {
|
|
1132
1356
|
})
|
|
1133
1357
|
);
|
|
1134
1358
|
}
|
|
@@ -1136,7 +1360,7 @@ async function preCache(root = document, options = {}) {
|
|
|
1136
1360
|
}
|
|
1137
1361
|
}
|
|
1138
1362
|
if (embedFonts) {
|
|
1139
|
-
await embedCustomFonts({
|
|
1363
|
+
await embedCustomFonts({ preCached: true });
|
|
1140
1364
|
}
|
|
1141
1365
|
await Promise.all(promises);
|
|
1142
1366
|
}
|