@zumer/snapdom 0.9.2 → 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 +102 -49
- package/dist/snapdom.js +552 -369
- package/dist/snapdom.min.js +3 -1
- package/dist/snapdom.min.mjs +3 -1
- package/dist/snapdom.mjs +553 -369
- package/package.json +1 -1
package/dist/snapdom.mjs
CHANGED
|
@@ -1,23 +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
|
-
|
|
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);
|
|
88
|
+
const defaults = {};
|
|
89
|
+
for (let prop of styles) {
|
|
90
|
+
defaults[prop] = styles.getPropertyValue(prop);
|
|
91
|
+
}
|
|
92
|
+
sandbox.removeChild(el);
|
|
93
|
+
defaultStylesCache.set(tagName, defaults);
|
|
94
|
+
return defaults;
|
|
95
|
+
}
|
|
96
|
+
function getStyleKey(snapshot, tagName, compress = false) {
|
|
11
97
|
const entries = [];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
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
|
+
}
|
|
16
109
|
}
|
|
17
110
|
}
|
|
18
111
|
return entries.sort().join(";");
|
|
19
112
|
}
|
|
20
|
-
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) {
|
|
21
142
|
const keySet = new Set(styleMap.values());
|
|
22
143
|
const classMap = /* @__PURE__ */ new Map();
|
|
23
144
|
let counter = 1;
|
|
@@ -27,19 +148,118 @@ function generateReusableCSSClasses(styleMap) {
|
|
|
27
148
|
return classMap;
|
|
28
149
|
}
|
|
29
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
|
+
|
|
30
248
|
// src/modules/styles.js
|
|
31
|
-
function inlineAllStyles(source, clone, styleMap,
|
|
249
|
+
function inlineAllStyles(source, clone, styleMap, cache, compress) {
|
|
32
250
|
if (source.tagName === "STYLE") return;
|
|
33
251
|
if (!cache.has(source)) {
|
|
34
|
-
cache.set(source,
|
|
252
|
+
cache.set(source, getStyle(source));
|
|
35
253
|
}
|
|
36
254
|
const style = cache.get(source);
|
|
37
|
-
const
|
|
255
|
+
const snapshot = snapshotComputedStyle(style);
|
|
256
|
+
const tagName = source.tagName?.toLowerCase() || "div";
|
|
257
|
+
const key = getStyleKey(snapshot, tagName, compress);
|
|
38
258
|
styleMap.set(clone, key);
|
|
39
259
|
}
|
|
40
260
|
|
|
41
261
|
// src/core/clone.js
|
|
42
|
-
function
|
|
262
|
+
function deepClone(node, styleMap, styleCache, nodeMap, compress) {
|
|
43
263
|
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
|
|
44
264
|
const spacer = document.createElement("div");
|
|
45
265
|
const rect = node.getBoundingClientRect();
|
|
@@ -55,7 +275,7 @@ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
|
|
|
55
275
|
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
|
|
56
276
|
const clone2 = node.cloneNode(false);
|
|
57
277
|
nodeMap.set(clone2, node);
|
|
58
|
-
inlineAllStyles(node, clone2, styleMap,
|
|
278
|
+
inlineAllStyles(node, clone2, styleMap, styleCache, compress);
|
|
59
279
|
const placeholder = document.createElement("div");
|
|
60
280
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
61
281
|
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
|
|
@@ -82,15 +302,15 @@ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
|
|
|
82
302
|
if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
|
|
83
303
|
const clone = node.cloneNode(false);
|
|
84
304
|
nodeMap.set(clone, node);
|
|
85
|
-
inlineAllStyles(node, clone, styleMap,
|
|
305
|
+
inlineAllStyles(node, clone, styleMap, styleCache, compress);
|
|
86
306
|
const frag = document.createDocumentFragment();
|
|
87
307
|
node.childNodes.forEach((child) => {
|
|
88
|
-
const clonedChild =
|
|
308
|
+
const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
|
|
89
309
|
if (clonedChild) frag.appendChild(clonedChild);
|
|
90
310
|
});
|
|
91
311
|
clone.appendChild(frag);
|
|
92
312
|
if (node.shadowRoot) {
|
|
93
|
-
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);
|
|
94
314
|
const shadowFrag = document.createDocumentFragment();
|
|
95
315
|
shadowContent.forEach((child) => shadowFrag.appendChild(child));
|
|
96
316
|
clone.appendChild(shadowFrag);
|
|
@@ -98,173 +318,107 @@ function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
|
|
|
98
318
|
return clone;
|
|
99
319
|
}
|
|
100
320
|
|
|
101
|
-
// src/
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const ctx = canvas.getContext("2d");
|
|
117
|
-
ctx.drawImage(image, 0, 0);
|
|
118
|
-
try {
|
|
119
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
120
|
-
resolve(dataURL);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
reject(new Error("CORS restrictions prevented image capture"));
|
|
123
|
-
}
|
|
124
|
-
} catch (e) {
|
|
125
|
-
reject(e);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
image.onerror = (e) => {
|
|
129
|
-
clearTimeout(timeoutId);
|
|
130
|
-
reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
|
|
131
|
-
};
|
|
132
|
-
image.src = src;
|
|
133
|
-
});
|
|
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();
|
|
134
336
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const processImg = async (img) => {
|
|
140
|
-
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) {
|
|
141
341
|
try {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
382
|
}
|
|
176
383
|
}
|
|
384
|
+
finalCSS += cssFinal + "\n";
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.warn("\u274C No pude fetch CSS", link.href);
|
|
177
387
|
}
|
|
178
|
-
const sChildren = Array.from(srcNode.children);
|
|
179
|
-
const cChildren = Array.from(cloneNode.children);
|
|
180
|
-
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
181
|
-
queue.push([sChildren[i], cChildren[i]]);
|
|
182
|
-
}
|
|
183
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;
|
|
184
396
|
}
|
|
185
397
|
|
|
186
398
|
// src/modules/pseudo.js
|
|
187
|
-
async function inlinePseudoElements(source, clone) {
|
|
188
|
-
|
|
189
|
-
"position",
|
|
190
|
-
"top",
|
|
191
|
-
"right",
|
|
192
|
-
"bottom",
|
|
193
|
-
"left",
|
|
194
|
-
"z-index",
|
|
195
|
-
"width",
|
|
196
|
-
"height",
|
|
197
|
-
"min-width",
|
|
198
|
-
"max-width",
|
|
199
|
-
"min-height",
|
|
200
|
-
"max-height",
|
|
201
|
-
"margin",
|
|
202
|
-
"padding",
|
|
203
|
-
"border",
|
|
204
|
-
"border-width",
|
|
205
|
-
"border-style",
|
|
206
|
-
"border-color",
|
|
207
|
-
"border-radius",
|
|
208
|
-
"box-sizing",
|
|
209
|
-
"display",
|
|
210
|
-
"overflow",
|
|
211
|
-
"overflow-x",
|
|
212
|
-
"overflow-y",
|
|
213
|
-
"flex",
|
|
214
|
-
"flex-grow",
|
|
215
|
-
"flex-shrink",
|
|
216
|
-
"flex-basis",
|
|
217
|
-
"flex-direction",
|
|
218
|
-
"justify-content",
|
|
219
|
-
"align-items",
|
|
220
|
-
"align-content",
|
|
221
|
-
"gap",
|
|
222
|
-
"row-gap",
|
|
223
|
-
"column-gap",
|
|
224
|
-
"grid",
|
|
225
|
-
"grid-template-columns",
|
|
226
|
-
"grid-template-rows",
|
|
227
|
-
"background",
|
|
228
|
-
"background-color",
|
|
229
|
-
"background-image",
|
|
230
|
-
"color",
|
|
231
|
-
"font",
|
|
232
|
-
"font-family",
|
|
233
|
-
"font-size",
|
|
234
|
-
"font-weight",
|
|
235
|
-
"font-style",
|
|
236
|
-
"font-stretch",
|
|
237
|
-
"text-align",
|
|
238
|
-
"vertical-align",
|
|
239
|
-
"text-decoration",
|
|
240
|
-
"line-height",
|
|
241
|
-
"letter-spacing",
|
|
242
|
-
"white-space",
|
|
243
|
-
"word-break",
|
|
244
|
-
"text-overflow",
|
|
245
|
-
"visibility",
|
|
246
|
-
"opacity",
|
|
247
|
-
"box-shadow",
|
|
248
|
-
"transform",
|
|
249
|
-
"transition",
|
|
250
|
-
"clip-path",
|
|
251
|
-
"-webkit-text-fill-color"
|
|
252
|
-
];
|
|
399
|
+
async function inlinePseudoElements(source, clone, styleMap, styleCache, compress) {
|
|
400
|
+
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
253
401
|
for (const pseudo of ["::before", "::after"]) {
|
|
254
402
|
try {
|
|
255
|
-
const style =
|
|
403
|
+
const style = getStyle(source, pseudo);
|
|
404
|
+
if (!style) continue;
|
|
256
405
|
const content = style.getPropertyValue("content");
|
|
257
406
|
const bg = style.getPropertyValue("background-image");
|
|
258
|
-
|
|
407
|
+
const hasContent = content && content !== "none" && content !== '""' && content !== "''";
|
|
408
|
+
const hasBg = bg && bg.startsWith("url(");
|
|
409
|
+
if (hasContent || hasBg) {
|
|
259
410
|
const fontFamily = style.getPropertyValue("font-family");
|
|
260
|
-
const pseudoEl = document.createElement("span");
|
|
261
|
-
pseudoEl.className = `snapdom-pseudo-${pseudo.replace("::", "")}`;
|
|
262
|
-
let cleanContent = parseContent(content);
|
|
263
|
-
const isIconFont = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(fontFamily);
|
|
264
411
|
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
|
|
265
412
|
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
|
|
266
413
|
const color = style.getPropertyValue("color") || "#000";
|
|
267
|
-
|
|
414
|
+
const pseudoEl = document.createElement("span");
|
|
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) {
|
|
268
422
|
const imgEl = document.createElement("img");
|
|
269
423
|
imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
270
424
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
@@ -274,7 +428,7 @@ async function inlinePseudoElements(source, clone) {
|
|
|
274
428
|
if (match?.[1]) {
|
|
275
429
|
try {
|
|
276
430
|
const imgEl = document.createElement("img");
|
|
277
|
-
const dataUrl = await
|
|
431
|
+
const dataUrl = await fetchImage(match[1]);
|
|
278
432
|
imgEl.src = dataUrl;
|
|
279
433
|
imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
|
|
280
434
|
pseudoEl.appendChild(imgEl);
|
|
@@ -282,16 +436,9 @@ async function inlinePseudoElements(source, clone) {
|
|
|
282
436
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
283
437
|
}
|
|
284
438
|
}
|
|
285
|
-
} else {
|
|
439
|
+
} else if (cleanContent && cleanContent !== "none") {
|
|
286
440
|
pseudoEl.textContent = cleanContent;
|
|
287
441
|
}
|
|
288
|
-
for (const prop of importantProps) {
|
|
289
|
-
const value = style.getPropertyValue(prop);
|
|
290
|
-
if (value) {
|
|
291
|
-
pseudoEl.style[prop] = value;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (!pseudoEl.style.position) pseudoEl.style.position = "absolute";
|
|
295
442
|
if (pseudo === "::before") {
|
|
296
443
|
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
297
444
|
} else {
|
|
@@ -299,171 +446,41 @@ async function inlinePseudoElements(source, clone) {
|
|
|
299
446
|
}
|
|
300
447
|
}
|
|
301
448
|
} catch (e) {
|
|
449
|
+
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
302
450
|
}
|
|
303
451
|
}
|
|
304
452
|
const sChildren = Array.from(source.children);
|
|
305
453
|
const cChildren = Array.from(clone.children).filter(
|
|
306
|
-
(child) => !child.
|
|
454
|
+
(child) => !child.dataset.snapdomPseudo
|
|
307
455
|
);
|
|
308
456
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
309
|
-
await inlinePseudoElements(sChildren[i], cChildren[i]);
|
|
457
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, compress);
|
|
310
458
|
}
|
|
311
459
|
}
|
|
312
|
-
function parseContent(content) {
|
|
313
|
-
let clean = content.replace(/^['"]|['"]$/g, "");
|
|
314
|
-
if (clean.startsWith("\\")) {
|
|
315
|
-
try {
|
|
316
|
-
return String.fromCharCode(parseInt(clean.replace("\\", ""), 16));
|
|
317
|
-
} catch {
|
|
318
|
-
return clean;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return clean;
|
|
322
|
-
}
|
|
323
|
-
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
324
|
-
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
325
|
-
await document.fonts.ready;
|
|
326
|
-
await document.fonts.load(`${fontSize}px "${fontFamily}"`);
|
|
327
|
-
const canvas = document.createElement("canvas");
|
|
328
|
-
canvas.width = fontSize;
|
|
329
|
-
canvas.height = fontSize;
|
|
330
|
-
const ctx = canvas.getContext("2d");
|
|
331
|
-
ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : ` ${fontSize}px "${fontFamily}"`;
|
|
332
|
-
ctx.textAlign = "center";
|
|
333
|
-
ctx.textBaseline = "middle";
|
|
334
|
-
ctx.fillStyle = color;
|
|
335
|
-
ctx.fillText(unicodeChar, canvas.width / 2, canvas.height / 2);
|
|
336
|
-
return canvas.toDataURL();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// src/utils/normalize.js
|
|
340
|
-
function normalizeFontName(str) {
|
|
341
|
-
return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
342
|
-
}
|
|
343
|
-
function normalizeVariation(value) {
|
|
344
|
-
const mappings = {
|
|
345
|
-
"normal": "400",
|
|
346
|
-
"bold": "700",
|
|
347
|
-
"italic": "italic",
|
|
348
|
-
"oblique": "italic",
|
|
349
|
-
"ultra-condensed": "50",
|
|
350
|
-
"extra-condensed": "62",
|
|
351
|
-
"condensed": "75",
|
|
352
|
-
"semi-condensed": "87",
|
|
353
|
-
"expanded": "125",
|
|
354
|
-
"semi-expanded": "112",
|
|
355
|
-
"extra-expanded": "150",
|
|
356
|
-
"ultra-expanded": "200"
|
|
357
|
-
};
|
|
358
|
-
return mappings[value] || value.replace(/%/g, "");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// src/modules/fonts.js
|
|
362
|
-
async function inlineFonts(styleCache = /* @__PURE__ */ new WeakMap()) {
|
|
363
|
-
await document.fonts.ready;
|
|
364
|
-
const fontFaceRules = [];
|
|
365
|
-
Array.from(document.styleSheets).forEach((sheet) => {
|
|
366
|
-
try {
|
|
367
|
-
Array.from(sheet.cssRules).forEach((rule) => {
|
|
368
|
-
if (rule instanceof CSSFontFaceRule) {
|
|
369
|
-
fontFaceRules.push(rule.cssText);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
} catch (e) {
|
|
373
|
-
console.warn("Could not access stylesheet rules:", e);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
const fontData = /* @__PURE__ */ new Map();
|
|
377
|
-
const elements = document.querySelectorAll("*");
|
|
378
|
-
elements.forEach((el) => {
|
|
379
|
-
const style = styleCache.get(el) || window.getComputedStyle(el);
|
|
380
|
-
if (!styleCache.has(el)) styleCache.set(el, style);
|
|
381
|
-
const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
|
|
382
|
-
if (!family) return;
|
|
383
|
-
const weight = normalizeVariation(style.fontWeight || "400");
|
|
384
|
-
const styleAttr = normalizeVariation(style.fontStyle || "normal");
|
|
385
|
-
const stretch = normalizeVariation(style.fontStretch || "normal");
|
|
386
|
-
if (!fontData.has(family)) {
|
|
387
|
-
fontData.set(family, /* @__PURE__ */ new Set());
|
|
388
|
-
}
|
|
389
|
-
fontData.get(family).add(`${weight}_${styleAttr}_${stretch}`);
|
|
390
|
-
});
|
|
391
|
-
const cssClasses = [...fontFaceRules];
|
|
392
|
-
for (const [family, variations] of fontData) {
|
|
393
|
-
const safeFamily = normalizeFontName(family);
|
|
394
|
-
for (const variation of variations) {
|
|
395
|
-
const [weight, styleAttr, stretch] = variation.split("_");
|
|
396
|
-
const variationClass = `fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`;
|
|
397
|
-
const cssProps = [
|
|
398
|
-
`font-family:"${family}",sans-serif`,
|
|
399
|
-
`font-weight:${weight}`,
|
|
400
|
-
`font-style:${styleAttr}`,
|
|
401
|
-
`font-stretch:${stretch}%`
|
|
402
|
-
].filter(Boolean).join(";");
|
|
403
|
-
cssClasses.push(`.${variationClass}{${cssProps}}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
cssClasses.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}");
|
|
407
|
-
return cssClasses.join("");
|
|
408
|
-
}
|
|
409
|
-
function applyFontClasses(clone, original, styleCache) {
|
|
410
|
-
const style = styleCache.get(original) || window.getComputedStyle(original);
|
|
411
|
-
const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
|
|
412
|
-
if (!family) return;
|
|
413
|
-
const weight = normalizeVariation(style.fontWeight || "400");
|
|
414
|
-
const styleAttr = normalizeVariation(style.fontStyle || "normal");
|
|
415
|
-
const stretch = normalizeVariation(style.fontStretch || "normal");
|
|
416
|
-
const safeFamily = normalizeFontName(family);
|
|
417
|
-
clone.classList.add(`fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`);
|
|
418
|
-
}
|
|
419
460
|
|
|
420
461
|
// src/core/prepare.js
|
|
421
|
-
async function prepareClone(element) {
|
|
462
|
+
async function prepareClone(element, compress = false) {
|
|
422
463
|
const styleMap = /* @__PURE__ */ new Map();
|
|
423
464
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
424
465
|
const nodeMap = /* @__PURE__ */ new WeakMap();
|
|
425
|
-
let defaults;
|
|
426
|
-
try {
|
|
427
|
-
defaults = {};
|
|
428
|
-
} catch (e) {
|
|
429
|
-
console.warn("getDefaultStyles failed:", e);
|
|
430
|
-
defaults = {};
|
|
431
|
-
}
|
|
432
466
|
let clone;
|
|
433
467
|
try {
|
|
434
|
-
clone =
|
|
468
|
+
clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
|
|
435
469
|
} catch (e) {
|
|
436
|
-
console.warn("
|
|
470
|
+
console.warn("deepClone failed:", e);
|
|
437
471
|
throw e;
|
|
438
472
|
}
|
|
439
473
|
try {
|
|
440
|
-
await
|
|
441
|
-
} catch (e) {
|
|
442
|
-
console.warn("inlineImages failed:", e);
|
|
443
|
-
}
|
|
444
|
-
try {
|
|
445
|
-
await inlineBackgroundImages(element, clone, styleCache);
|
|
446
|
-
} catch (e) {
|
|
447
|
-
console.warn("inlineBackgroundImages failed:", e);
|
|
448
|
-
}
|
|
449
|
-
try {
|
|
450
|
-
await inlinePseudoElements(element, clone);
|
|
474
|
+
await inlinePseudoElements(element, clone, styleMap, styleCache, compress);
|
|
451
475
|
} catch (e) {
|
|
452
476
|
console.warn("inlinePseudoElements failed:", e);
|
|
453
477
|
}
|
|
454
|
-
const keyToClass =
|
|
478
|
+
const keyToClass = generateCSSClasses(styleMap);
|
|
455
479
|
const classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
456
480
|
for (const [node, key] of styleMap.entries()) {
|
|
457
481
|
if (node.tagName === "STYLE") continue;
|
|
458
482
|
const className = keyToClass.get(key);
|
|
459
|
-
if (className)
|
|
460
|
-
node.classList.add(className);
|
|
461
|
-
try {
|
|
462
|
-
applyFontClasses(node, nodeMap.get(node), styleCache);
|
|
463
|
-
} catch (e) {
|
|
464
|
-
console.warn("applyFontClasses failed for node:", node, e);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
483
|
+
if (className) node.classList.add(className);
|
|
467
484
|
const bgImage = node.style?.backgroundImage;
|
|
468
485
|
node.removeAttribute("style");
|
|
469
486
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
@@ -471,84 +488,179 @@ async function prepareClone(element) {
|
|
|
471
488
|
return { clone, classCSS, styleCache };
|
|
472
489
|
}
|
|
473
490
|
|
|
474
|
-
// src/
|
|
475
|
-
function
|
|
476
|
-
|
|
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
|
+
}
|
|
477
546
|
}
|
|
478
547
|
|
|
479
548
|
// src/core/capture.js
|
|
480
|
-
async function
|
|
481
|
-
|
|
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;
|
|
482
552
|
let clone, classCSS, styleCache;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
});
|
|
488
577
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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;
|
|
520
629
|
}
|
|
521
630
|
}
|
|
522
631
|
|
|
523
632
|
// src/api/snapdom.js
|
|
524
633
|
async function snapdom(element, options = {}) {
|
|
525
|
-
|
|
634
|
+
if (!element) {
|
|
635
|
+
throw new Error("Element cannot be null or undefined");
|
|
636
|
+
}
|
|
637
|
+
return await captureDOM(element, options);
|
|
526
638
|
}
|
|
527
|
-
snapdom.toImg = async (el,
|
|
528
|
-
const url = await snapdom(el,
|
|
639
|
+
snapdom.toImg = async (el, options = {}) => {
|
|
640
|
+
const url = await snapdom(el, options);
|
|
529
641
|
const img = new Image();
|
|
530
642
|
img.src = url;
|
|
531
643
|
await img.decode();
|
|
532
644
|
return img;
|
|
533
645
|
};
|
|
534
|
-
snapdom.toCanvas = async (el,
|
|
535
|
-
const img = await snapdom.toImg(el,
|
|
646
|
+
snapdom.toCanvas = async (el, options = {}) => {
|
|
647
|
+
const img = await snapdom.toImg(el, options);
|
|
536
648
|
const canvas = document.createElement("canvas");
|
|
537
649
|
canvas.width = img.width;
|
|
538
650
|
canvas.height = img.height;
|
|
539
651
|
canvas.getContext("2d").drawImage(img, 0, 0);
|
|
540
652
|
return canvas;
|
|
541
653
|
};
|
|
542
|
-
snapdom.toPng = async (el,
|
|
543
|
-
const canvas = await snapdom.toCanvas(el,
|
|
654
|
+
snapdom.toPng = async (el, options = {}) => {
|
|
655
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
544
656
|
const img = new Image();
|
|
545
657
|
img.src = canvas.toDataURL("image/png");
|
|
546
658
|
await img.decode();
|
|
547
659
|
return img;
|
|
548
660
|
};
|
|
549
|
-
snapdom.toJpg = async (el,
|
|
550
|
-
const { backgroundColor = "#fff", quality =
|
|
551
|
-
const canvas = await snapdom.toCanvas(el,
|
|
661
|
+
snapdom.toJpg = async (el, options = {}) => {
|
|
662
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
663
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
552
664
|
const temp = document.createElement("canvas");
|
|
553
665
|
temp.width = canvas.width;
|
|
554
666
|
temp.height = canvas.height;
|
|
@@ -561,9 +673,9 @@ snapdom.toJpg = async (el, opt = {}) => {
|
|
|
561
673
|
await img.decode();
|
|
562
674
|
return img;
|
|
563
675
|
};
|
|
564
|
-
snapdom.toWebp = async (el,
|
|
565
|
-
const { backgroundColor = "#fff", quality =
|
|
566
|
-
const canvas = await snapdom.toCanvas(el,
|
|
676
|
+
snapdom.toWebp = async (el, options = {}) => {
|
|
677
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
678
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
567
679
|
const temp = document.createElement("canvas");
|
|
568
680
|
temp.width = canvas.width;
|
|
569
681
|
temp.height = canvas.height;
|
|
@@ -576,11 +688,83 @@ snapdom.toWebp = async (el, opt = {}) => {
|
|
|
576
688
|
await img.decode();
|
|
577
689
|
return img;
|
|
578
690
|
};
|
|
579
|
-
snapdom.toBlob = async (el,
|
|
580
|
-
const dataUrl = await snapdom(el,
|
|
691
|
+
snapdom.toBlob = async (el, options = {}) => {
|
|
692
|
+
const dataUrl = await snapdom(el, options);
|
|
581
693
|
const svgText = decodeURIComponent(dataUrl.split(",")[1]);
|
|
582
694
|
return new Blob([svgText], { type: "image/svg+xml" });
|
|
583
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
|
+
}
|
|
584
767
|
export {
|
|
768
|
+
preCache,
|
|
585
769
|
snapdom
|
|
586
770
|
};
|