@zumer/snapdom 0.9.2 → 0.9.8
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 +105 -49
- package/dist/snapdom.js +547 -369
- package/dist/snapdom.min.js +3 -1
- package/dist/snapdom.min.mjs +3 -1
- package/dist/snapdom.mjs +548 -369
- package/package.json +1 -1
package/dist/snapdom.js
CHANGED
|
@@ -1,23 +1,144 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.0.9.
|
|
4
|
+
* v.0.9.8
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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]);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
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
|
-
}
|
|
457
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, compress);
|
|
405
458
|
}
|
|
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
459
|
}
|
|
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,174 @@
|
|
|
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 dataURL;
|
|
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
|
-
fo.setAttribute("width", "100%");
|
|
504
|
-
fo.setAttribute("height", "100%");
|
|
505
|
-
const div = document.createElement("div");
|
|
506
|
-
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
507
|
-
div.setAttribute("style", `width:${w}px;height:${h}px;`);
|
|
508
|
-
const styleTag = document.createElement("style");
|
|
509
|
-
styleTag.textContent = fonts + "svg{overflow:visible}" + classCSS;
|
|
510
|
-
div.appendChild(styleTag);
|
|
511
|
-
div.appendChild(clone);
|
|
512
|
-
fo.appendChild(div);
|
|
513
|
-
svg.appendChild(fo);
|
|
514
|
-
try {
|
|
515
|
-
const svgStr = new XMLSerializer().serializeToString(svg);
|
|
516
|
-
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(minifySVG(svgStr))}`;
|
|
517
|
-
} catch (e) {
|
|
518
|
-
console.error("SVG serialization failed:", e);
|
|
519
|
-
throw e;
|
|
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
|
+
}
|
|
520
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
|
+
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
619
|
+
resolve();
|
|
620
|
+
}, { fast });
|
|
621
|
+
});
|
|
622
|
+
const sandbox = document.getElementById("snapdom-sandbox");
|
|
623
|
+
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
624
|
+
return dataURL;
|
|
521
625
|
}
|
|
522
626
|
|
|
523
627
|
// src/api/snapdom.js
|
|
524
628
|
async function snapdom(element, options = {}) {
|
|
525
|
-
|
|
629
|
+
if (!element) {
|
|
630
|
+
throw new Error("Element cannot be null or undefined");
|
|
631
|
+
}
|
|
632
|
+
return await captureDOM(element, options);
|
|
526
633
|
}
|
|
527
|
-
snapdom.toImg = async (el,
|
|
528
|
-
const url = await snapdom(el,
|
|
634
|
+
snapdom.toImg = async (el, options = {}) => {
|
|
635
|
+
const url = await snapdom(el, options);
|
|
529
636
|
const img = new Image();
|
|
530
637
|
img.src = url;
|
|
531
638
|
await img.decode();
|
|
532
639
|
return img;
|
|
533
640
|
};
|
|
534
|
-
snapdom.toCanvas = async (el,
|
|
535
|
-
const img = await snapdom.toImg(el,
|
|
641
|
+
snapdom.toCanvas = async (el, options = {}) => {
|
|
642
|
+
const img = await snapdom.toImg(el, options);
|
|
536
643
|
const canvas = document.createElement("canvas");
|
|
537
644
|
canvas.width = img.width;
|
|
538
645
|
canvas.height = img.height;
|
|
539
646
|
canvas.getContext("2d").drawImage(img, 0, 0);
|
|
540
647
|
return canvas;
|
|
541
648
|
};
|
|
542
|
-
snapdom.toPng = async (el,
|
|
543
|
-
const canvas = await snapdom.toCanvas(el,
|
|
649
|
+
snapdom.toPng = async (el, options = {}) => {
|
|
650
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
544
651
|
const img = new Image();
|
|
545
652
|
img.src = canvas.toDataURL("image/png");
|
|
546
653
|
await img.decode();
|
|
547
654
|
return img;
|
|
548
655
|
};
|
|
549
|
-
snapdom.toJpg = async (el,
|
|
550
|
-
const { backgroundColor = "#fff", quality =
|
|
551
|
-
const canvas = await snapdom.toCanvas(el,
|
|
656
|
+
snapdom.toJpg = async (el, options = {}) => {
|
|
657
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
658
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
552
659
|
const temp = document.createElement("canvas");
|
|
553
660
|
temp.width = canvas.width;
|
|
554
661
|
temp.height = canvas.height;
|
|
@@ -561,9 +668,9 @@
|
|
|
561
668
|
await img.decode();
|
|
562
669
|
return img;
|
|
563
670
|
};
|
|
564
|
-
snapdom.toWebp = async (el,
|
|
565
|
-
const { backgroundColor = "#fff", quality =
|
|
566
|
-
const canvas = await snapdom.toCanvas(el,
|
|
671
|
+
snapdom.toWebp = async (el, options = {}) => {
|
|
672
|
+
const { backgroundColor = "#fff", quality = 1 } = options;
|
|
673
|
+
const canvas = await snapdom.toCanvas(el, options);
|
|
567
674
|
const temp = document.createElement("canvas");
|
|
568
675
|
temp.width = canvas.width;
|
|
569
676
|
temp.height = canvas.height;
|
|
@@ -576,9 +683,80 @@
|
|
|
576
683
|
await img.decode();
|
|
577
684
|
return img;
|
|
578
685
|
};
|
|
579
|
-
snapdom.toBlob = async (el,
|
|
580
|
-
const dataUrl = await snapdom(el,
|
|
686
|
+
snapdom.toBlob = async (el, options = {}) => {
|
|
687
|
+
const dataUrl = await snapdom(el, options);
|
|
581
688
|
const svgText = decodeURIComponent(dataUrl.split(",")[1]);
|
|
582
689
|
return new Blob([svgText], { type: "image/svg+xml" });
|
|
583
690
|
};
|
|
691
|
+
|
|
692
|
+
// src/api/preCache.js
|
|
693
|
+
async function preCache(root = document, options = {}) {
|
|
694
|
+
const { embedFonts = true, reset = false, preWarm = true } = options;
|
|
695
|
+
if (reset) {
|
|
696
|
+
imageCache.clear();
|
|
697
|
+
bgCache.clear();
|
|
698
|
+
resourceCache.clear();
|
|
699
|
+
baseCSSCache.clear();
|
|
700
|
+
computedStyleCache.clear();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
await document.fonts.ready;
|
|
704
|
+
precacheCommonTags();
|
|
705
|
+
let imgEls = [], allEls = [];
|
|
706
|
+
if (root?.querySelectorAll) {
|
|
707
|
+
imgEls = Array.from(root.querySelectorAll("img[src]"));
|
|
708
|
+
allEls = Array.from(root.querySelectorAll("*"));
|
|
709
|
+
}
|
|
710
|
+
const promises = [];
|
|
711
|
+
for (const img of imgEls) {
|
|
712
|
+
const src = img.src;
|
|
713
|
+
if (!imageCache.has(src)) {
|
|
714
|
+
promises.push(
|
|
715
|
+
fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (const el of allEls) {
|
|
721
|
+
const bg = getComputedStyle(el).backgroundImage;
|
|
722
|
+
const url = extractUrl(bg);
|
|
723
|
+
if (url && !bgCache.has(url)) {
|
|
724
|
+
promises.push(
|
|
725
|
+
fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (embedFonts) {
|
|
731
|
+
await embedCustomFonts({ ignoreIconFonts: true, preCached: true });
|
|
732
|
+
}
|
|
733
|
+
await Promise.all(promises);
|
|
734
|
+
if (preWarm) {
|
|
735
|
+
const usedTags = collectUsedTagNames(root).sort();
|
|
736
|
+
const tagKey = usedTags.join(",");
|
|
737
|
+
if (!baseCSSCache.has(tagKey)) {
|
|
738
|
+
const css = generateDedupedBaseCSS(usedTags);
|
|
739
|
+
baseCSSCache.set(tagKey, css);
|
|
740
|
+
}
|
|
741
|
+
const clone = root.cloneNode(false);
|
|
742
|
+
await inlinePseudoElements(
|
|
743
|
+
root,
|
|
744
|
+
clone,
|
|
745
|
+
/* @__PURE__ */ new Map(),
|
|
746
|
+
/* @__PURE__ */ new WeakMap(),
|
|
747
|
+
/* compress */
|
|
748
|
+
false
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (preWarm) {
|
|
752
|
+
try {
|
|
753
|
+
await prepareClone(
|
|
754
|
+
root,
|
|
755
|
+
/* compress: */
|
|
756
|
+
options.preWarm
|
|
757
|
+
);
|
|
758
|
+
} catch (e) {
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
584
762
|
})();
|