@zumer/snapdom 0.9.1 → 0.9.7
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 +138 -73
- package/dist/snapdom.js +577 -334
- package/dist/snapdom.min.js +3 -1
- package/dist/snapdom.min.mjs +3 -1
- package/dist/snapdom.mjs +578 -334
- package/package.json +1 -1
package/dist/snapdom.mjs
CHANGED
|
@@ -1,34 +1,144 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.0.9.
|
|
4
|
+
* v.0.9.7
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
8
8
|
|
|
9
|
+
// src/core/cache.js
|
|
10
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
11
|
+
var bgCache = /* @__PURE__ */ new Map();
|
|
12
|
+
var resourceCache = /* @__PURE__ */ new Map();
|
|
13
|
+
var defaultStylesCache = /* @__PURE__ */ new Map();
|
|
14
|
+
var baseCSSCache = /* @__PURE__ */ new Map();
|
|
15
|
+
var computedStyleCache = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
|
|
9
17
|
// src/utils/cssTools.js
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
var commonTags = [
|
|
19
|
+
"div",
|
|
20
|
+
"span",
|
|
21
|
+
"p",
|
|
22
|
+
"a",
|
|
23
|
+
"img",
|
|
24
|
+
"ul",
|
|
25
|
+
"li",
|
|
26
|
+
"button",
|
|
27
|
+
"input",
|
|
28
|
+
"select",
|
|
29
|
+
"textarea",
|
|
30
|
+
"label",
|
|
31
|
+
"section",
|
|
32
|
+
"article",
|
|
33
|
+
"header",
|
|
34
|
+
"footer",
|
|
35
|
+
"nav",
|
|
36
|
+
"main",
|
|
37
|
+
"aside",
|
|
38
|
+
"h1",
|
|
39
|
+
"h2",
|
|
40
|
+
"h3",
|
|
41
|
+
"h4",
|
|
42
|
+
"h5",
|
|
43
|
+
"h6",
|
|
44
|
+
"svg",
|
|
45
|
+
"path",
|
|
46
|
+
"circle",
|
|
47
|
+
"rect",
|
|
48
|
+
"line",
|
|
49
|
+
"g",
|
|
50
|
+
"table",
|
|
51
|
+
"thead",
|
|
52
|
+
"tbody",
|
|
53
|
+
"tr",
|
|
54
|
+
"td",
|
|
55
|
+
"th"
|
|
56
|
+
];
|
|
57
|
+
function precacheCommonTags() {
|
|
58
|
+
for (let tag of commonTags) {
|
|
59
|
+
getDefaultStyleForTag(tag);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function getDefaultStyleForTag(tagName) {
|
|
63
|
+
if (defaultStylesCache.has(tagName)) {
|
|
64
|
+
return defaultStylesCache.get(tagName);
|
|
65
|
+
}
|
|
66
|
+
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
|
|
67
|
+
if (skipTags.has(tagName)) {
|
|
68
|
+
const empty = {};
|
|
69
|
+
defaultStylesCache.set(tagName, empty);
|
|
70
|
+
return empty;
|
|
71
|
+
}
|
|
72
|
+
let sandbox = document.getElementById("snapdom-sandbox");
|
|
73
|
+
if (!sandbox) {
|
|
74
|
+
sandbox = document.createElement("div");
|
|
75
|
+
sandbox.id = "snapdom-sandbox";
|
|
76
|
+
sandbox.style.position = "absolute";
|
|
77
|
+
sandbox.style.left = "-9999px";
|
|
78
|
+
sandbox.style.top = "-9999px";
|
|
79
|
+
sandbox.style.width = "0";
|
|
80
|
+
sandbox.style.height = "0";
|
|
81
|
+
sandbox.style.overflow = "hidden";
|
|
82
|
+
document.body.appendChild(sandbox);
|
|
83
|
+
}
|
|
84
|
+
const el = document.createElement(tagName);
|
|
85
|
+
el.style.all = "initial";
|
|
86
|
+
sandbox.appendChild(el);
|
|
87
|
+
const styles = getComputedStyle(el);
|
|
14
88
|
const defaults = {};
|
|
15
89
|
for (let prop of styles) {
|
|
16
90
|
defaults[prop] = styles.getPropertyValue(prop);
|
|
17
91
|
}
|
|
18
|
-
|
|
92
|
+
sandbox.removeChild(el);
|
|
93
|
+
defaultStylesCache.set(tagName, defaults);
|
|
19
94
|
return defaults;
|
|
20
95
|
}
|
|
21
|
-
function getStyleKey(
|
|
96
|
+
function getStyleKey(snapshot, tagName, compress = false) {
|
|
22
97
|
const entries = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
98
|
+
const defaultStyles = getDefaultStyleForTag(tagName);
|
|
99
|
+
for (let [prop, value] of Object.entries(snapshot)) {
|
|
100
|
+
if (!compress) {
|
|
101
|
+
if (value) {
|
|
102
|
+
entries.push(`${prop}:${value}`);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
const defaultValue = defaultStyles[prop];
|
|
106
|
+
if (value && value !== defaultValue) {
|
|
107
|
+
entries.push(`${prop}:${value}`);
|
|
108
|
+
}
|
|
27
109
|
}
|
|
28
110
|
}
|
|
29
111
|
return entries.sort().join(";");
|
|
30
112
|
}
|
|
31
|
-
function
|
|
113
|
+
function collectUsedTagNames(root) {
|
|
114
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
115
|
+
if (root.tagName) {
|
|
116
|
+
tagSet.add(root.tagName.toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
root.querySelectorAll("*").forEach((el) => {
|
|
119
|
+
tagSet.add(el.tagName.toLowerCase());
|
|
120
|
+
});
|
|
121
|
+
return Array.from(tagSet);
|
|
122
|
+
}
|
|
123
|
+
function generateDedupedBaseCSS(usedTagNames) {
|
|
124
|
+
const groups = /* @__PURE__ */ new Map();
|
|
125
|
+
for (let tagName of usedTagNames) {
|
|
126
|
+
const styles = defaultStylesCache.get(tagName);
|
|
127
|
+
if (!styles) continue;
|
|
128
|
+
const key = Object.entries(styles).map(([k, v]) => `${k}:${v};`).sort().join("");
|
|
129
|
+
if (!groups.has(key)) {
|
|
130
|
+
groups.set(key, []);
|
|
131
|
+
}
|
|
132
|
+
groups.get(key).push(tagName);
|
|
133
|
+
}
|
|
134
|
+
let css = "";
|
|
135
|
+
for (let [styleBlock, tagList] of groups.entries()) {
|
|
136
|
+
css += `${tagList.join(",")} { ${styleBlock} }
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
return css;
|
|
140
|
+
}
|
|
141
|
+
function generateCSSClasses(styleMap) {
|
|
32
142
|
const keySet = new Set(styleMap.values());
|
|
33
143
|
const classMap = /* @__PURE__ */ new Map();
|
|
34
144
|
let counter = 1;
|
|
@@ -38,19 +148,118 @@ function generateReusableCSSClasses(styleMap) {
|
|
|
38
148
|
return classMap;
|
|
39
149
|
}
|
|
40
150
|
|
|
151
|
+
// src/utils/helpers.js
|
|
152
|
+
function idle(fn, { fast = false } = {}) {
|
|
153
|
+
if (fast) return fn();
|
|
154
|
+
if ("requestIdleCallback" in window) {
|
|
155
|
+
requestIdleCallback(fn, { timeout: 50 });
|
|
156
|
+
} else {
|
|
157
|
+
setTimeout(fn, 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function getStyle(el, pseudo = null) {
|
|
161
|
+
if (!(el instanceof Element)) {
|
|
162
|
+
return window.getComputedStyle(el, pseudo);
|
|
163
|
+
}
|
|
164
|
+
let map = computedStyleCache.get(el);
|
|
165
|
+
if (!map) {
|
|
166
|
+
map = /* @__PURE__ */ new Map();
|
|
167
|
+
computedStyleCache.set(el, map);
|
|
168
|
+
}
|
|
169
|
+
if (!map.has(pseudo)) {
|
|
170
|
+
const st = window.getComputedStyle(el, pseudo);
|
|
171
|
+
map.set(pseudo, st);
|
|
172
|
+
}
|
|
173
|
+
return map.get(pseudo);
|
|
174
|
+
}
|
|
175
|
+
function parseContent(content) {
|
|
176
|
+
let clean = content.replace(/^['"]|['"]$/g, "");
|
|
177
|
+
if (clean.startsWith("\\")) {
|
|
178
|
+
try {
|
|
179
|
+
return String.fromCharCode(parseInt(clean.replace("\\", ""), 16));
|
|
180
|
+
} catch {
|
|
181
|
+
return clean;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return clean;
|
|
185
|
+
}
|
|
186
|
+
function extractUrl(cssValue) {
|
|
187
|
+
const m = cssValue.match(/url\(["']?([^"')]+)["']?\)/);
|
|
188
|
+
return m ? m[1] : null;
|
|
189
|
+
}
|
|
190
|
+
function isIconFont(familyOrUrl) {
|
|
191
|
+
const iconFontPatterns = [
|
|
192
|
+
/font\s*awesome/i,
|
|
193
|
+
/material\s*icons/i,
|
|
194
|
+
/ionicons/i,
|
|
195
|
+
/glyphicons/i,
|
|
196
|
+
/feather/i,
|
|
197
|
+
/bootstrap\s*icons/i,
|
|
198
|
+
/remix\s*icons/i,
|
|
199
|
+
/heroicons/i
|
|
200
|
+
];
|
|
201
|
+
return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
|
|
202
|
+
}
|
|
203
|
+
function fetchImage(src, timeout = 3e3) {
|
|
204
|
+
if (imageCache.has(src)) {
|
|
205
|
+
return Promise.resolve(imageCache.get(src));
|
|
206
|
+
}
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const timeoutId = setTimeout(() => {
|
|
209
|
+
reject(new Error("Image load timed out"));
|
|
210
|
+
}, timeout);
|
|
211
|
+
const image = new Image();
|
|
212
|
+
image.crossOrigin = "anonymous";
|
|
213
|
+
image.onload = async () => {
|
|
214
|
+
clearTimeout(timeoutId);
|
|
215
|
+
try {
|
|
216
|
+
await image.decode();
|
|
217
|
+
const canvas = document.createElement("canvas");
|
|
218
|
+
canvas.width = image.width;
|
|
219
|
+
canvas.height = image.height;
|
|
220
|
+
const ctx = canvas.getContext("2d");
|
|
221
|
+
ctx.drawImage(image, 0, 0);
|
|
222
|
+
try {
|
|
223
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
224
|
+
imageCache.set(src, dataURL);
|
|
225
|
+
resolve(dataURL);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
reject(new Error("CORS restrictions prevented image capture"));
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
reject(e);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
image.onerror = (e) => {
|
|
234
|
+
clearTimeout(timeoutId);
|
|
235
|
+
reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
|
|
236
|
+
};
|
|
237
|
+
image.src = src;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function snapshotComputedStyle(style) {
|
|
241
|
+
const snap = {};
|
|
242
|
+
for (let prop of style) {
|
|
243
|
+
snap[prop] = style.getPropertyValue(prop);
|
|
244
|
+
}
|
|
245
|
+
return snap;
|
|
246
|
+
}
|
|
247
|
+
|
|
41
248
|
// src/modules/styles.js
|
|
42
|
-
function inlineAllStyles(source, clone, styleMap,
|
|
249
|
+
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
43
250
|
if (source.tagName === "STYLE") return;
|
|
44
251
|
if (!cache.has(source)) {
|
|
45
|
-
cache.set(source,
|
|
252
|
+
cache.set(source, getStyle(source));
|
|
46
253
|
}
|
|
47
254
|
const style = cache.get(source);
|
|
48
|
-
const
|
|
255
|
+
const snapshot = snapshotComputedStyle(style);
|
|
256
|
+
const tagName = source.tagName?.toLowerCase() || "div";
|
|
257
|
+
const key = getStyleKey(snapshot, tagName, compress);
|
|
49
258
|
styleMap.set(clone, key);
|
|
50
259
|
}
|
|
51
260
|
|
|
52
261
|
// src/core/clone.js
|
|
53
|
-
function
|
|
262
|
+
function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
54
263
|
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
|
|
55
264
|
const spacer = document.createElement("div");
|
|
56
265
|
const rect = node.getBoundingClientRect();
|
|
@@ -66,7 +275,7 @@ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
|
|
|
66
275
|
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
|
|
67
276
|
const clone2 = node.cloneNode(false);
|
|
68
277
|
nodeMap.set(clone2, node);
|
|
69
|
-
inlineAllStyles(node, clone2, styleMap,
|
|
278
|
+
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
70
279
|
const placeholder = document.createElement("div");
|
|
71
280
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
72
281
|
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
|
|
@@ -93,187 +302,143 @@ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
|
|
|
93
302
|
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
94
303
|
const clone = node.cloneNode(false);
|
|
95
304
|
nodeMap.set(clone, node);
|
|
96
|
-
inlineAllStyles(node, clone, styleMap,
|
|
305
|
+
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
97
306
|
const frag = document.createDocumentFragment();
|
|
98
307
|
node.childNodes.forEach((child) => {
|
|
99
|
-
const clonedChild =
|
|
308
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
|
|
100
309
|
if (clonedChild) frag.appendChild(clonedChild);
|
|
101
310
|
});
|
|
102
311
|
clone.appendChild(frag);
|
|
103
312
|
if (node.shadowRoot) {
|
|
104
|
-
const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) =>
|
|
313
|
+
const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap)).filter(Boolean);
|
|
105
314
|
const shadowFrag = document.createDocumentFragment();
|
|
106
315
|
shadowContent.forEach((child) => shadowFrag.appendChild(child));
|
|
107
316
|
clone.appendChild(shadowFrag);
|
|
108
317
|
}
|
|
109
|
-
clone.querySelectorAll("style").forEach((tag) => tag.removeAttribute("style"));
|
|
110
|
-
if (clone.tagName === "STYLE") clone.removeAttribute("style");
|
|
111
318
|
return clone;
|
|
112
319
|
}
|
|
113
320
|
|
|
114
|
-
// src/
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const ctx = canvas.getContext("2d");
|
|
130
|
-
ctx.drawImage(image, 0, 0);
|
|
131
|
-
try {
|
|
132
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
133
|
-
resolve(dataURL);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
reject(new Error("CORS restrictions prevented image capture"));
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
reject(e);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
image.onerror = (e) => {
|
|
142
|
-
clearTimeout(timeoutId);
|
|
143
|
-
reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
|
|
144
|
-
};
|
|
145
|
-
image.src = src;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/utils/delay.js
|
|
150
|
-
function delay(ms = 0) {
|
|
151
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
321
|
+
// src/modules/fonts.js
|
|
322
|
+
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
323
|
+
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
324
|
+
await document.fonts.ready;
|
|
325
|
+
await document.fonts.load(`${fontSize}px "${fontFamily}"`);
|
|
326
|
+
const canvas = document.createElement("canvas");
|
|
327
|
+
canvas.width = fontSize;
|
|
328
|
+
canvas.height = fontSize;
|
|
329
|
+
const ctx = canvas.getContext("2d");
|
|
330
|
+
ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : ` ${fontSize}px "${fontFamily}"`;
|
|
331
|
+
ctx.textAlign = "center";
|
|
332
|
+
ctx.textBaseline = "middle";
|
|
333
|
+
ctx.fillStyle = color;
|
|
334
|
+
ctx.fillText(unicodeChar, canvas.width / 2, canvas.height / 2);
|
|
335
|
+
return canvas.toDataURL();
|
|
152
336
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const processImg = async (img) => {
|
|
158
|
-
const src = img.src;
|
|
337
|
+
async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
|
|
338
|
+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
|
|
339
|
+
let finalCSS = "";
|
|
340
|
+
for (const link of links) {
|
|
159
341
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
342
|
+
const res = await fetch(link.href);
|
|
343
|
+
const cssText = await res.text();
|
|
344
|
+
if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) {
|
|
345
|
+
console.log("\u23ED\uFE0F Ignorando icon font CSS:", link.href);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const urlRegex = /url\(([^)]+)\)/g;
|
|
349
|
+
const inlinedCSS = await Promise.all(
|
|
350
|
+
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
|
|
351
|
+
let url = match[1].replace(/["']/g, "");
|
|
352
|
+
if (!url.startsWith("http")) {
|
|
353
|
+
url = new URL(url, link.href).href;
|
|
354
|
+
}
|
|
355
|
+
if (ignoreIconFonts && isIconFont(url)) {
|
|
356
|
+
console.log("\u23ED\uFE0F Ignorando icon font URL:", url);
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (resourceCache.has(url)) {
|
|
360
|
+
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const fontRes = await fetch(url);
|
|
364
|
+
const blob = await fontRes.blob();
|
|
365
|
+
const b64 = await new Promise((resolve) => {
|
|
366
|
+
const reader = new FileReader();
|
|
367
|
+
reader.onload = () => resolve(reader.result);
|
|
368
|
+
reader.readAsDataURL(blob);
|
|
369
|
+
});
|
|
370
|
+
resourceCache.set(url, b64);
|
|
371
|
+
return { original: match[0], inlined: `url(${b64})` };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.warn("\u274C No pude fetch font", url);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
let cssFinal = cssText;
|
|
379
|
+
for (const r of inlinedCSS) {
|
|
380
|
+
if (r) {
|
|
381
|
+
cssFinal = cssFinal.replace(r.original, r.inlined);
|
|
194
382
|
}
|
|
195
383
|
}
|
|
384
|
+
finalCSS += cssFinal + "\n";
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.warn("\u274C No pude fetch CSS", link.href);
|
|
196
387
|
}
|
|
197
|
-
const sChildren = Array.from(srcNode.children);
|
|
198
|
-
const cChildren = Array.from(cloneNode.children);
|
|
199
|
-
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
200
|
-
queue.push([sChildren[i], cChildren[i]]);
|
|
201
|
-
}
|
|
202
|
-
await delay(1);
|
|
203
388
|
}
|
|
389
|
+
if (finalCSS && preCached) {
|
|
390
|
+
const style = document.createElement("style");
|
|
391
|
+
style.setAttribute("data-snapdom", "embedFonts");
|
|
392
|
+
style.textContent = finalCSS;
|
|
393
|
+
document.head.appendChild(style);
|
|
394
|
+
}
|
|
395
|
+
return finalCSS;
|
|
204
396
|
}
|
|
205
397
|
|
|
206
398
|
// src/modules/pseudo.js
|
|
207
|
-
async function inlinePseudoElements(source, clone) {
|
|
208
|
-
|
|
209
|
-
"position",
|
|
210
|
-
"top",
|
|
211
|
-
"right",
|
|
212
|
-
"bottom",
|
|
213
|
-
"left",
|
|
214
|
-
"z-index",
|
|
215
|
-
"width",
|
|
216
|
-
"height",
|
|
217
|
-
"min-width",
|
|
218
|
-
"max-width",
|
|
219
|
-
"min-height",
|
|
220
|
-
"max-height",
|
|
221
|
-
"margin",
|
|
222
|
-
"padding",
|
|
223
|
-
"border",
|
|
224
|
-
"border-radius",
|
|
225
|
-
"background",
|
|
226
|
-
"background-color",
|
|
227
|
-
"background-image",
|
|
228
|
-
"color",
|
|
229
|
-
"font",
|
|
230
|
-
"font-family",
|
|
231
|
-
"font-size",
|
|
232
|
-
"font-weight",
|
|
233
|
-
"text-align",
|
|
234
|
-
"text-decoration",
|
|
235
|
-
"line-height",
|
|
236
|
-
"letter-spacing",
|
|
237
|
-
"display",
|
|
238
|
-
"visibility",
|
|
239
|
-
"opacity",
|
|
240
|
-
"overflow",
|
|
241
|
-
"box-shadow",
|
|
242
|
-
"transform",
|
|
243
|
-
"transition",
|
|
244
|
-
"flex",
|
|
245
|
-
"grid"
|
|
246
|
-
];
|
|
399
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress) {
|
|
400
|
+
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
247
401
|
for (const pseudo of ["::before", "::after"]) {
|
|
248
402
|
try {
|
|
249
|
-
const style =
|
|
403
|
+
const style = getStyle(source, pseudo);
|
|
404
|
+
if (!style) continue;
|
|
250
405
|
const content = style.getPropertyValue("content");
|
|
251
|
-
|
|
406
|
+
const bg = style.getPropertyValue("background-image");
|
|
407
|
+
const hasContent = content && content !== "none" && content !== '""' && content !== "''";
|
|
408
|
+
const hasBg = bg && bg.startsWith("url(");
|
|
409
|
+
if (hasContent || hasBg) {
|
|
410
|
+
const fontFamily = style.getPropertyValue("font-family");
|
|
411
|
+
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
|
|
412
|
+
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
|
|
413
|
+
const color = style.getPropertyValue("color") || "#000";
|
|
252
414
|
const pseudoEl = document.createElement("span");
|
|
253
|
-
pseudoEl.
|
|
254
|
-
|
|
255
|
-
|
|
415
|
+
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
416
|
+
const snapshot = snapshotComputedStyle(style);
|
|
417
|
+
const key = getStyleKey(snapshot, "span", compress);
|
|
418
|
+
styleMap.set(pseudoEl, key);
|
|
419
|
+
const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(fontFamily);
|
|
420
|
+
let cleanContent = parseContent(content);
|
|
421
|
+
if (isIconFont2 && cleanContent.length === 1) {
|
|
422
|
+
const imgEl = document.createElement("img");
|
|
423
|
+
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
424
|
+
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
425
|
+
pseudoEl.appendChild(imgEl);
|
|
426
|
+
} else if (cleanContent.startsWith("url(")) {
|
|
256
427
|
const match = cleanContent.match(/url\(["']?([^"')]+)["']?\)/);
|
|
257
428
|
if (match?.[1]) {
|
|
258
429
|
try {
|
|
259
430
|
const imgEl = document.createElement("img");
|
|
260
|
-
const dataUrl = await
|
|
431
|
+
const dataUrl = await fetchImage(match[1]);
|
|
261
432
|
imgEl.src = dataUrl;
|
|
262
433
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
263
434
|
pseudoEl.appendChild(imgEl);
|
|
264
435
|
} catch (e) {
|
|
436
|
+
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
265
437
|
}
|
|
266
438
|
}
|
|
267
|
-
} else {
|
|
439
|
+
} else if (cleanContent && cleanContent !== "none") {
|
|
268
440
|
pseudoEl.textContent = cleanContent;
|
|
269
441
|
}
|
|
270
|
-
for (const prop of importantProps) {
|
|
271
|
-
const value = style.getPropertyValue(prop);
|
|
272
|
-
if (value) {
|
|
273
|
-
pseudoEl.style[prop] = value;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (!pseudoEl.style.position) pseudoEl.style.position = "absolute";
|
|
277
442
|
if (pseudo === "::before") {
|
|
278
443
|
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
279
444
|
} else {
|
|
@@ -281,146 +446,41 @@ async function inlinePseudoElements(source, clone) {
|
|
|
281
446
|
}
|
|
282
447
|
}
|
|
283
448
|
} catch (e) {
|
|
449
|
+
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
284
450
|
}
|
|
285
451
|
}
|
|
286
452
|
const sChildren = Array.from(source.children);
|
|
287
453
|
const cChildren = Array.from(clone.children).filter(
|
|
288
|
-
(child) => !child.
|
|
454
|
+
(child) => !child.dataset.snapdomPseudo
|
|
289
455
|
);
|
|
290
456
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
291
|
-
await inlinePseudoElements(sChildren[i], cChildren[i]);
|
|
292
|
-
if (i % 5 === 0) await delay(1);
|
|
457
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, compress);
|
|
293
458
|
}
|
|
294
459
|
}
|
|
295
460
|
|
|
296
|
-
// src/utils/normalize.js
|
|
297
|
-
function normalizeFontName(str) {
|
|
298
|
-
return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
299
|
-
}
|
|
300
|
-
function normalizeVariation(value) {
|
|
301
|
-
const mappings = {
|
|
302
|
-
"normal": "400",
|
|
303
|
-
"bold": "700",
|
|
304
|
-
"italic": "italic",
|
|
305
|
-
"oblique": "italic",
|
|
306
|
-
"ultra-condensed": "50",
|
|
307
|
-
"extra-condensed": "62",
|
|
308
|
-
"condensed": "75",
|
|
309
|
-
"semi-condensed": "87",
|
|
310
|
-
"expanded": "125",
|
|
311
|
-
"semi-expanded": "112",
|
|
312
|
-
"extra-expanded": "150",
|
|
313
|
-
"ultra-expanded": "200"
|
|
314
|
-
};
|
|
315
|
-
return mappings[value] || value.replace(/%/g, "");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// src/modules/fonts.js
|
|
319
|
-
async function inlineFonts(styleCache = /* @__PURE__ */ new WeakMap()) {
|
|
320
|
-
await document.fonts.ready;
|
|
321
|
-
const fontFaceRules = [];
|
|
322
|
-
Array.from(document.styleSheets).forEach((sheet) => {
|
|
323
|
-
try {
|
|
324
|
-
Array.from(sheet.cssRules).forEach((rule) => {
|
|
325
|
-
if (rule instanceof CSSFontFaceRule) {
|
|
326
|
-
fontFaceRules.push(rule.cssText);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
} catch (e) {
|
|
330
|
-
console.warn("Could not access stylesheet rules:", e);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
const fontData = /* @__PURE__ */ new Map();
|
|
334
|
-
const elements = document.querySelectorAll("*");
|
|
335
|
-
elements.forEach((el) => {
|
|
336
|
-
const style = styleCache.get(el) || window.getComputedStyle(el);
|
|
337
|
-
if (!styleCache.has(el)) styleCache.set(el, style);
|
|
338
|
-
const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
|
|
339
|
-
if (!family) return;
|
|
340
|
-
const weight = normalizeVariation(style.fontWeight || "400");
|
|
341
|
-
const styleAttr = normalizeVariation(style.fontStyle || "normal");
|
|
342
|
-
const stretch = normalizeVariation(style.fontStretch || "normal");
|
|
343
|
-
if (!fontData.has(family)) {
|
|
344
|
-
fontData.set(family, /* @__PURE__ */ new Set());
|
|
345
|
-
}
|
|
346
|
-
fontData.get(family).add(`${weight}_${styleAttr}_${stretch}`);
|
|
347
|
-
});
|
|
348
|
-
const cssClasses = [...fontFaceRules];
|
|
349
|
-
for (const [family, variations] of fontData) {
|
|
350
|
-
const safeFamily = normalizeFontName(family);
|
|
351
|
-
for (const variation of variations) {
|
|
352
|
-
const [weight, styleAttr, stretch] = variation.split("_");
|
|
353
|
-
const variationClass = `fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`;
|
|
354
|
-
const cssProps = [
|
|
355
|
-
`font-family:"${family}",sans-serif`,
|
|
356
|
-
`font-weight:${weight}`,
|
|
357
|
-
`font-style:${styleAttr}`,
|
|
358
|
-
`font-stretch:${stretch}%`
|
|
359
|
-
].filter(Boolean).join(";");
|
|
360
|
-
cssClasses.push(`.${variationClass}{${cssProps}}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
cssClasses.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}");
|
|
364
|
-
return cssClasses.join("");
|
|
365
|
-
}
|
|
366
|
-
function applyFontClasses(clone, original, styleCache) {
|
|
367
|
-
const style = styleCache.get(original) || window.getComputedStyle(original);
|
|
368
|
-
const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
|
|
369
|
-
if (!family) return;
|
|
370
|
-
const weight = normalizeVariation(style.fontWeight || "400");
|
|
371
|
-
const styleAttr = normalizeVariation(style.fontStyle || "normal");
|
|
372
|
-
const stretch = normalizeVariation(style.fontStretch || "normal");
|
|
373
|
-
const safeFamily = normalizeFontName(family);
|
|
374
|
-
clone.classList.add(`fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
461
|
// src/core/prepare.js
|
|
378
|
-
async function prepareClone(element) {
|
|
462
|
+
async function prepareClone(element, compress = false) {
|
|
379
463
|
const styleMap = /* @__PURE__ */ new Map();
|
|
380
464
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
381
465
|
const nodeMap = /* @__PURE__ */ new WeakMap();
|
|
382
|
-
let defaults;
|
|
383
|
-
try {
|
|
384
|
-
defaults = getDefaultStyles();
|
|
385
|
-
} catch (e) {
|
|
386
|
-
console.warn("getDefaultStyles failed:", e);
|
|
387
|
-
defaults = {};
|
|
388
|
-
}
|
|
389
466
|
let clone;
|
|
390
467
|
try {
|
|
391
|
-
clone =
|
|
468
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
|
|
392
469
|
} catch (e) {
|
|
393
|
-
console.warn("
|
|
470
|
+
console.warn("deepClone failed:", e);
|
|
394
471
|
throw e;
|
|
395
472
|
}
|
|
396
473
|
try {
|
|
397
|
-
await
|
|
398
|
-
} catch (e) {
|
|
399
|
-
console.warn("inlineImages failed:", e);
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
await inlineBackgroundImages(element, clone, styleCache);
|
|
403
|
-
} catch (e) {
|
|
404
|
-
console.warn("inlineBackgroundImages failed:", e);
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
await inlinePseudoElements(element, clone);
|
|
474
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress);
|
|
408
475
|
} catch (e) {
|
|
409
476
|
console.warn("inlinePseudoElements failed:", e);
|
|
410
477
|
}
|
|
411
|
-
const keyToClass =
|
|
478
|
+
const keyToClass = generateCSSClasses(styleMap);
|
|
412
479
|
const classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
413
480
|
for (const [node, key] of styleMap.entries()) {
|
|
414
481
|
if (node.tagName === "STYLE") continue;
|
|
415
482
|
const className = keyToClass.get(key);
|
|
416
|
-
if (className)
|
|
417
|
-
node.classList.add(className);
|
|
418
|
-
try {
|
|
419
|
-
applyFontClasses(node, nodeMap.get(node), styleCache);
|
|
420
|
-
} catch (e) {
|
|
421
|
-
console.warn("applyFontClasses failed for node:", node, e);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
483
|
+
if (className) node.classList.add(className);
|
|
424
484
|
const bgImage = node.style?.backgroundImage;
|
|
425
485
|
node.removeAttribute("style");
|
|
426
486
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
@@ -428,99 +488,283 @@ async function prepareClone(element) {
|
|
|
428
488
|
return { clone, classCSS, styleCache };
|
|
429
489
|
}
|
|
430
490
|
|
|
431
|
-
// src/
|
|
432
|
-
function
|
|
433
|
-
|
|
491
|
+
// src/modules/images.js
|
|
492
|
+
async function inlineImages(clone) {
|
|
493
|
+
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
494
|
+
const processImg = async (img) => {
|
|
495
|
+
const src = img.src;
|
|
496
|
+
try {
|
|
497
|
+
const dataUrl = await fetchImage(src);
|
|
498
|
+
img.src = dataUrl;
|
|
499
|
+
if (!img.width) img.width = img.naturalWidth || 100;
|
|
500
|
+
if (!img.height) img.height = img.naturalHeight || 100;
|
|
501
|
+
} catch {
|
|
502
|
+
const fallback = document.createElement("div");
|
|
503
|
+
fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
|
|
504
|
+
fallback.innerText = "img";
|
|
505
|
+
img.replaceWith(fallback);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
for (let i = 0; i < imgs.length; i += 4) {
|
|
509
|
+
const group = imgs.slice(i, i + 4).map(processImg);
|
|
510
|
+
await Promise.allSettled(group);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/modules/background.js
|
|
515
|
+
async function inlineBackgroundImages(source, clone, styleCache) {
|
|
516
|
+
const queue = [[source, clone]];
|
|
517
|
+
while (queue.length) {
|
|
518
|
+
const [srcNode, cloneNode] = queue.shift();
|
|
519
|
+
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
520
|
+
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
521
|
+
const bg = style.getPropertyValue("background-image");
|
|
522
|
+
if (bg && bg.includes("url(")) {
|
|
523
|
+
const match = bg.match(/url\(["']?([^"')]+)["']?\)/);
|
|
524
|
+
if (match?.[1]) {
|
|
525
|
+
try {
|
|
526
|
+
const bgUrl = match[1];
|
|
527
|
+
let dataUrl;
|
|
528
|
+
if (bgCache.has(bgUrl)) {
|
|
529
|
+
dataUrl = bgCache.get(bgUrl);
|
|
530
|
+
} else {
|
|
531
|
+
dataUrl = await fetchImage(bgUrl);
|
|
532
|
+
bgCache.set(bgUrl, dataUrl);
|
|
533
|
+
}
|
|
534
|
+
cloneNode.style.backgroundImage = `url(${dataUrl})`;
|
|
535
|
+
} catch {
|
|
536
|
+
cloneNode.style.backgroundImage = "none";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const sChildren = Array.from(srcNode.children);
|
|
541
|
+
const cChildren = Array.from(cloneNode.children);
|
|
542
|
+
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
543
|
+
queue.push([sChildren[i], cChildren[i]]);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
434
546
|
}
|
|
435
547
|
|
|
436
548
|
// src/core/capture.js
|
|
437
|
-
async function
|
|
549
|
+
async function captureDOM(element, options = {}) {
|
|
550
|
+
if (!element) throw new Error("Element cannot be null or undefined");
|
|
551
|
+
const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
|
|
438
552
|
let clone, classCSS, styleCache;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
553
|
+
let fontsCSS = "";
|
|
554
|
+
let baseCSS = "";
|
|
555
|
+
let dataBlob;
|
|
556
|
+
let svgString;
|
|
557
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, compress));
|
|
558
|
+
await new Promise((resolve) => {
|
|
559
|
+
idle(async () => {
|
|
560
|
+
await inlineImages(clone);
|
|
561
|
+
resolve();
|
|
562
|
+
}, { fast });
|
|
563
|
+
});
|
|
564
|
+
await new Promise((resolve) => {
|
|
565
|
+
idle(async () => {
|
|
566
|
+
await inlineBackgroundImages(element, clone, styleCache);
|
|
567
|
+
resolve();
|
|
568
|
+
}, { fast });
|
|
569
|
+
});
|
|
570
|
+
if (embedFonts) {
|
|
571
|
+
await new Promise((resolve) => {
|
|
572
|
+
idle(async () => {
|
|
573
|
+
fontsCSS = await embedCustomFonts({ ignoreIconFonts: true });
|
|
574
|
+
resolve();
|
|
575
|
+
}, { fast });
|
|
576
|
+
});
|
|
444
577
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
578
|
+
if (compress) {
|
|
579
|
+
const usedTags = collectUsedTagNames(clone).sort();
|
|
580
|
+
const tagKey = usedTags.join(",");
|
|
581
|
+
if (baseCSSCache.has(tagKey)) {
|
|
582
|
+
baseCSS = baseCSSCache.get(tagKey);
|
|
583
|
+
} else {
|
|
584
|
+
await new Promise((resolve) => {
|
|
585
|
+
idle(() => {
|
|
586
|
+
baseCSS = generateDedupedBaseCSS(usedTags);
|
|
587
|
+
baseCSSCache.set(tagKey, baseCSS);
|
|
588
|
+
resolve();
|
|
589
|
+
}, { fast });
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await new Promise((resolve) => {
|
|
594
|
+
idle(() => {
|
|
595
|
+
const rect = element.getBoundingClientRect();
|
|
596
|
+
const w = rect.width * scale;
|
|
597
|
+
const h = rect.height * scale;
|
|
598
|
+
if (scale !== 1) {
|
|
599
|
+
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
600
|
+
clone.style.transform = `scale(${scale})`;
|
|
601
|
+
clone.style.transformOrigin = "top left";
|
|
602
|
+
clone.style.width = `${rect.width}px`;
|
|
603
|
+
clone.style.height = `${rect.height}px`;
|
|
604
|
+
}
|
|
605
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
606
|
+
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
607
|
+
fo.setAttribute("width", "100%");
|
|
608
|
+
fo.setAttribute("height", "100%");
|
|
609
|
+
const styleTag = document.createElement("style");
|
|
610
|
+
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
|
|
611
|
+
fo.appendChild(styleTag);
|
|
612
|
+
fo.appendChild(clone);
|
|
613
|
+
const serializer = new XMLSerializer();
|
|
614
|
+
const foString = serializer.serializeToString(fo);
|
|
615
|
+
const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
|
|
616
|
+
const svgFooter = "</svg>";
|
|
617
|
+
svgString = svgHeader + foString + svgFooter;
|
|
618
|
+
const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
|
619
|
+
dataBlob = URL.createObjectURL(blob);
|
|
620
|
+
resolve();
|
|
621
|
+
}, { fast });
|
|
622
|
+
});
|
|
623
|
+
const sandbox = document.getElementById("snapdom-sandbox");
|
|
624
|
+
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
625
|
+
if (options.dataURL) {
|
|
626
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
627
|
+
} else {
|
|
628
|
+
return dataBlob;
|
|
475
629
|
}
|
|
476
630
|
}
|
|
477
631
|
|
|
478
632
|
// src/api/snapdom.js
|
|
479
|
-
async function snapdom(
|
|
480
|
-
|
|
633
|
+
async function snapdom(element, options = {}) {
|
|
634
|
+
if (!element) {
|
|
635
|
+
throw new Error("Element cannot be null or undefined");
|
|
636
|
+
}
|
|
637
|
+
return await captureDOM(element, options);
|
|
481
638
|
}
|
|
482
|
-
snapdom.toImg = async
|
|
483
|
-
const
|
|
639
|
+
snapdom.toImg = async (el, options = {}) => {
|
|
640
|
+
const url = await snapdom(el, options);
|
|
484
641
|
const img = new Image();
|
|
485
|
-
img.src =
|
|
642
|
+
img.src = url;
|
|
486
643
|
await img.decode();
|
|
487
644
|
return img;
|
|
488
645
|
};
|
|
489
|
-
snapdom.toCanvas = async
|
|
490
|
-
const img = await snapdom.toImg(el,
|
|
646
|
+
snapdom.toCanvas = async (el, options = {}) => {
|
|
647
|
+
const img = await snapdom.toImg(el, options);
|
|
491
648
|
const canvas = document.createElement("canvas");
|
|
492
649
|
canvas.width = img.width;
|
|
493
650
|
canvas.height = img.height;
|
|
494
|
-
|
|
495
|
-
ctx.drawImage(img, 0, 0);
|
|
651
|
+
canvas.getContext("2d").drawImage(img, 0, 0);
|
|
496
652
|
return canvas;
|
|
497
653
|
};
|
|
498
|
-
snapdom.toPng = async
|
|
499
|
-
const canvas = await snapdom.toCanvas(el,
|
|
654
|
+
snapdom.toPng = async (el, options = {}) => {
|
|
655
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
500
656
|
const img = new Image();
|
|
501
657
|
img.src = canvas.toDataURL("image/png");
|
|
502
658
|
await img.decode();
|
|
503
659
|
return img;
|
|
504
660
|
};
|
|
505
|
-
snapdom.toJpg = async
|
|
506
|
-
const
|
|
661
|
+
snapdom.toJpg = async (el, options = {}) => {
|
|
662
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
663
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
664
|
+
const temp = document.createElement("canvas");
|
|
665
|
+
temp.width = canvas.width;
|
|
666
|
+
temp.height = canvas.height;
|
|
667
|
+
const ctx = temp.getContext("2d");
|
|
668
|
+
ctx.fillStyle = backgroundColor;
|
|
669
|
+
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
670
|
+
ctx.drawImage(canvas, 0, 0);
|
|
507
671
|
const img = new Image();
|
|
508
|
-
img.src =
|
|
672
|
+
img.src = temp.toDataURL("image/jpeg", quality);
|
|
509
673
|
await img.decode();
|
|
510
674
|
return img;
|
|
511
675
|
};
|
|
512
|
-
snapdom.toWebp = async
|
|
513
|
-
const
|
|
676
|
+
snapdom.toWebp = async (el, options = {}) => {
|
|
677
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
678
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
679
|
+
const temp = document.createElement("canvas");
|
|
680
|
+
temp.width = canvas.width;
|
|
681
|
+
temp.height = canvas.height;
|
|
682
|
+
const ctx = temp.getContext("2d");
|
|
683
|
+
ctx.fillStyle = backgroundColor;
|
|
684
|
+
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
685
|
+
ctx.drawImage(canvas, 0, 0);
|
|
514
686
|
const img = new Image();
|
|
515
|
-
img.src =
|
|
687
|
+
img.src = temp.toDataURL("image/webp", quality);
|
|
516
688
|
await img.decode();
|
|
517
689
|
return img;
|
|
518
690
|
};
|
|
519
|
-
snapdom.toBlob = async
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
return new Blob([
|
|
691
|
+
snapdom.toBlob = async (el, options = {}) => {
|
|
692
|
+
const dataUrl = await snapdom(el, options);
|
|
693
|
+
const svgText = decodeURIComponent(dataUrl.split(",")[1]);
|
|
694
|
+
return new Blob([svgText], { type: "image/svg+xml" });
|
|
523
695
|
};
|
|
696
|
+
|
|
697
|
+
// src/api/preCache.js
|
|
698
|
+
async function preCache(root = document, options = {}) {
|
|
699
|
+
const { embedFonts = true, reset = false, preWarm = true } = options;
|
|
700
|
+
if (reset) {
|
|
701
|
+
imageCache.clear();
|
|
702
|
+
bgCache.clear();
|
|
703
|
+
resourceCache.clear();
|
|
704
|
+
baseCSSCache.clear();
|
|
705
|
+
computedStyleCache.clear();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
await document.fonts.ready;
|
|
709
|
+
precacheCommonTags();
|
|
710
|
+
let imgEls = [], allEls = [];
|
|
711
|
+
if (root?.querySelectorAll) {
|
|
712
|
+
imgEls = Array.from(root.querySelectorAll("img[src]"));
|
|
713
|
+
allEls = Array.from(root.querySelectorAll("*"));
|
|
714
|
+
}
|
|
715
|
+
const promises = [];
|
|
716
|
+
for (const img of imgEls) {
|
|
717
|
+
const src = img.src;
|
|
718
|
+
if (!imageCache.has(src)) {
|
|
719
|
+
promises.push(
|
|
720
|
+
fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const el of allEls) {
|
|
726
|
+
const bg = getComputedStyle(el).backgroundImage;
|
|
727
|
+
const url = extractUrl(bg);
|
|
728
|
+
if (url && !bgCache.has(url)) {
|
|
729
|
+
promises.push(
|
|
730
|
+
fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
731
|
+
})
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (embedFonts) {
|
|
736
|
+
await embedCustomFonts({ ignoreIconFonts: true, preCached: true });
|
|
737
|
+
}
|
|
738
|
+
await Promise.all(promises);
|
|
739
|
+
if (preWarm) {
|
|
740
|
+
const usedTags = collectUsedTagNames(root).sort();
|
|
741
|
+
const tagKey = usedTags.join(",");
|
|
742
|
+
if (!baseCSSCache.has(tagKey)) {
|
|
743
|
+
const css = generateDedupedBaseCSS(usedTags);
|
|
744
|
+
baseCSSCache.set(tagKey, css);
|
|
745
|
+
}
|
|
746
|
+
const clone = root.cloneNode(false);
|
|
747
|
+
await inlinePseudoElements(
|
|
748
|
+
root,
|
|
749
|
+
clone,
|
|
750
|
+
/* @__PURE__ */ new Map(),
|
|
751
|
+
/* @__PURE__ */ new WeakMap(),
|
|
752
|
+
/* compress */
|
|
753
|
+
false
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (preWarm) {
|
|
757
|
+
try {
|
|
758
|
+
await prepareClone(
|
|
759
|
+
root,
|
|
760
|
+
/* compress: */
|
|
761
|
+
options.preWarm
|
|
762
|
+
);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
524
767
|
export {
|
|
768
|
+
preCache,
|
|
525
769
|
snapdom
|
|
526
770
|
};
|