@zumer/snapdom 1.9.8 → 1.9.10-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -81
- package/dist/snapdom.js +1692 -690
- package/dist/snapdom.min.js +5 -3
- package/dist/snapdom.min.mjs +5 -3
- package/dist/snapdom.mjs +1651 -642
- package/package.json +1 -1
- package/types/snapdom.d.ts +171 -57
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.
|
|
4
|
+
* v.1.9.10-dev.0
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -16,23 +16,307 @@ var cache = {
|
|
|
16
16
|
baseStyle: /* @__PURE__ */ new Map(),
|
|
17
17
|
computedStyle: /* @__PURE__ */ new WeakMap(),
|
|
18
18
|
font: /* @__PURE__ */ new Set(),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
reset: resetCache
|
|
19
|
+
session: {
|
|
20
|
+
styleMap: /* @__PURE__ */ new Map(),
|
|
21
|
+
styleCache: /* @__PURE__ */ new WeakMap(),
|
|
22
|
+
nodeMap: /* @__PURE__ */ new Map()
|
|
23
|
+
}
|
|
25
24
|
};
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
function applyCachePolicy(policy = "soft") {
|
|
26
|
+
switch (policy) {
|
|
27
|
+
case "auto": {
|
|
28
|
+
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
29
|
+
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
case "soft": {
|
|
33
|
+
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
34
|
+
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
35
|
+
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
case "full": {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
case "disabled": {
|
|
42
|
+
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
43
|
+
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
44
|
+
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
cache.image.clear();
|
|
46
|
+
cache.background.clear();
|
|
47
|
+
cache.resource.clear();
|
|
48
|
+
cache.font.clear();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
default: {
|
|
52
|
+
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
53
|
+
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
54
|
+
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/utils/helpers.js
|
|
61
|
+
function extractURL(value) {
|
|
62
|
+
const match = value.match(/url\((['"]?)(.*?)(\1)\)/);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
const url = match[2].trim();
|
|
65
|
+
if (url.startsWith("#")) return null;
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
function stripTranslate(transform) {
|
|
69
|
+
if (!transform || transform === "none") return "";
|
|
70
|
+
let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
|
|
71
|
+
cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
|
|
72
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
73
|
+
if (parts.length !== 6) return `matrix(${values})`;
|
|
74
|
+
parts[4] = "0";
|
|
75
|
+
parts[5] = "0";
|
|
76
|
+
return `matrix(${parts.join(", ")})`;
|
|
77
|
+
});
|
|
78
|
+
cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
|
|
79
|
+
const parts = values.split(",").map((s) => s.trim());
|
|
80
|
+
if (parts.length !== 16) return `matrix3d(${values})`;
|
|
81
|
+
parts[12] = "0";
|
|
82
|
+
parts[13] = "0";
|
|
83
|
+
return `matrix3d(${parts.join(", ")})`;
|
|
84
|
+
});
|
|
85
|
+
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
86
|
+
}
|
|
87
|
+
function safeEncodeURI(uri) {
|
|
88
|
+
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
89
|
+
try {
|
|
90
|
+
return encodeURI(uri);
|
|
91
|
+
} catch {
|
|
92
|
+
return uri;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/modules/snapFetch.js
|
|
97
|
+
function createSnapLogger(prefix = "[snapDOM]", { ttlMs = 5 * 6e4, maxEntries = 12 } = {}) {
|
|
98
|
+
const seen = /* @__PURE__ */ new Map();
|
|
99
|
+
let emitted = 0;
|
|
100
|
+
function log(level, key, msg) {
|
|
101
|
+
if (emitted >= maxEntries) return;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const until = seen.get(key) || 0;
|
|
104
|
+
if (until > now) return;
|
|
105
|
+
seen.set(key, now + ttlMs);
|
|
106
|
+
emitted++;
|
|
107
|
+
if (level === "warn" && console && console.warn) console.warn(`${prefix} ${msg}`);
|
|
108
|
+
else if (console && console.error) console.error(`${prefix} ${msg}`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
warnOnce(key, msg) {
|
|
112
|
+
log("warn", key, msg);
|
|
113
|
+
},
|
|
114
|
+
errorOnce(key, msg) {
|
|
115
|
+
log("error", key, msg);
|
|
116
|
+
},
|
|
117
|
+
reset() {
|
|
118
|
+
seen.clear();
|
|
119
|
+
emitted = 0;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
var snapLogger = createSnapLogger("[snapDOM]", { ttlMs: 3 * 6e4, maxEntries: 10 });
|
|
124
|
+
var _inflight = /* @__PURE__ */ new Map();
|
|
125
|
+
var _errorCache = /* @__PURE__ */ new Map();
|
|
126
|
+
function isSpecialURL(url) {
|
|
127
|
+
return /^data:|^blob:|^about:blank$/.test(url);
|
|
128
|
+
}
|
|
129
|
+
function shouldProxy(url, useProxy) {
|
|
130
|
+
if (!useProxy || isSpecialURL(url)) return false;
|
|
131
|
+
try {
|
|
132
|
+
const base = typeof location !== "undefined" && location.href ? location.href : "http://localhost/";
|
|
133
|
+
const u = new URL(url, base);
|
|
134
|
+
return typeof location !== "undefined" ? u.origin !== location.origin : true;
|
|
135
|
+
} catch {
|
|
136
|
+
return !!useProxy;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function applyProxy(url, useProxy) {
|
|
140
|
+
if (!useProxy) return url;
|
|
141
|
+
if (useProxy.includes("{url}")) return useProxy.replace("{url}", encodeURIComponent(url));
|
|
142
|
+
const sep = useProxy.endsWith("?") ? "" : useProxy.includes("?") ? "&" : "?";
|
|
143
|
+
return `${useProxy}${sep}url=${encodeURIComponent(url)}`;
|
|
144
|
+
}
|
|
145
|
+
function blobToDataURL(blob) {
|
|
146
|
+
return new Promise((res, rej) => {
|
|
147
|
+
const fr = new FileReader();
|
|
148
|
+
fr.onload = () => res(String(fr.result || ""));
|
|
149
|
+
fr.onerror = () => rej(new Error("read_failed"));
|
|
150
|
+
fr.readAsDataURL(blob);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function makeKey(url, o) {
|
|
154
|
+
return [
|
|
155
|
+
o.as || "blob",
|
|
156
|
+
o.timeout ?? 3e3,
|
|
157
|
+
o.useProxy || "",
|
|
158
|
+
o.errorTTL ?? 8e3,
|
|
159
|
+
url
|
|
160
|
+
].join("|");
|
|
161
|
+
}
|
|
162
|
+
async function snapFetch(url, options = {}) {
|
|
163
|
+
const as = options.as ?? "blob";
|
|
164
|
+
const timeout = options.timeout ?? 3e3;
|
|
165
|
+
const useProxy = options.useProxy || "";
|
|
166
|
+
const errorTTL = options.errorTTL ?? 8e3;
|
|
167
|
+
const headers = options.headers || {};
|
|
168
|
+
const silent = !!options.silent;
|
|
169
|
+
const key = makeKey(url, { as, timeout, useProxy, errorTTL });
|
|
170
|
+
const e = _errorCache.get(key);
|
|
171
|
+
if (e && e.until > Date.now()) {
|
|
172
|
+
return { ...e.result, fromCache: true };
|
|
173
|
+
} else if (e) {
|
|
174
|
+
_errorCache.delete(key);
|
|
175
|
+
}
|
|
176
|
+
const inflight = _inflight.get(key);
|
|
177
|
+
if (inflight) return inflight;
|
|
178
|
+
const finalURL = shouldProxy(url, useProxy) ? applyProxy(url, useProxy) : url;
|
|
179
|
+
let cred = options.credentials;
|
|
180
|
+
if (!cred) {
|
|
181
|
+
try {
|
|
182
|
+
const base = typeof location !== "undefined" && location.href ? location.href : "http://localhost/";
|
|
183
|
+
const u = new URL(url, base);
|
|
184
|
+
const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
|
|
185
|
+
cred = sameOrigin ? "include" : "omit";
|
|
186
|
+
} catch {
|
|
187
|
+
cred = "omit";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const ctrl = new AbortController();
|
|
191
|
+
const timer = setTimeout(() => ctrl.abort("timeout"), timeout);
|
|
192
|
+
const p = (async () => {
|
|
193
|
+
try {
|
|
194
|
+
const resp = await fetch(finalURL, { signal: ctrl.signal, credentials: cred, headers });
|
|
195
|
+
if (!resp.ok) {
|
|
196
|
+
const result = { ok: false, data: null, status: resp.status, url: finalURL, fromCache: false, reason: "http_error" };
|
|
197
|
+
_errorCache.set(key, { until: Date.now() + errorTTL, result });
|
|
198
|
+
if (!silent) {
|
|
199
|
+
const short = `${resp.status} ${resp.statusText || ""}`.trim();
|
|
200
|
+
snapLogger.warnOnce(
|
|
201
|
+
`http:${resp.status}:${as}:${new URL(url, location?.href ?? "http://localhost/").origin}`,
|
|
202
|
+
`HTTP error ${short} while fetching ${as} ${url}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
options.onError && options.onError(result);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
if (as === "text") {
|
|
209
|
+
const text = await resp.text();
|
|
210
|
+
return { ok: true, data: text, status: resp.status, url: finalURL, fromCache: false };
|
|
211
|
+
}
|
|
212
|
+
const blob = await resp.blob();
|
|
213
|
+
const mime = blob.type || resp.headers.get("content-type") || "";
|
|
214
|
+
if (as === "dataURL") {
|
|
215
|
+
const dataURL = await blobToDataURL(blob);
|
|
216
|
+
return { ok: true, data: dataURL, status: resp.status, url: finalURL, fromCache: false, mime };
|
|
217
|
+
}
|
|
218
|
+
return { ok: true, data: blob, status: resp.status, url: finalURL, fromCache: false, mime };
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const reason = err && typeof err === "object" && "name" in err && err.name === "AbortError" ? String(err.message || "").includes("timeout") ? "timeout" : "abort" : "network";
|
|
221
|
+
const result = { ok: false, data: null, status: 0, url: finalURL, fromCache: false, reason };
|
|
222
|
+
_errorCache.set(key, { until: Date.now() + errorTTL, result });
|
|
223
|
+
if (!silent) {
|
|
224
|
+
const k = `${reason}:${as}:${new URL(url, location?.href ?? "http://localhost/").origin}`;
|
|
225
|
+
const tips = reason === "timeout" ? `Timeout after ${timeout}ms. Consider increasing timeout or using a proxy for ${url}` : reason === "abort" ? `Request aborted while fetching ${as} ${url}` : `Network/CORS issue while fetching ${as} ${url}. A proxy may be required`;
|
|
226
|
+
snapLogger.errorOnce(k, tips);
|
|
227
|
+
}
|
|
228
|
+
options.onError && options.onError(result);
|
|
229
|
+
return result;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
_inflight.delete(key);
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
_inflight.set(key, p);
|
|
236
|
+
return p;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/utils/image.js
|
|
240
|
+
function createBackground(baseCanvas, backgroundColor) {
|
|
241
|
+
if (!backgroundColor || !baseCanvas.width || !baseCanvas.height) {
|
|
242
|
+
return baseCanvas;
|
|
243
|
+
}
|
|
244
|
+
const temp = document.createElement("canvas");
|
|
245
|
+
temp.width = baseCanvas.width;
|
|
246
|
+
temp.height = baseCanvas.height;
|
|
247
|
+
const ctx = temp.getContext("2d");
|
|
248
|
+
ctx.fillStyle = backgroundColor;
|
|
249
|
+
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
250
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
251
|
+
return temp;
|
|
252
|
+
}
|
|
253
|
+
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
254
|
+
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
255
|
+
if (isGradient || entry.trim() === "none") {
|
|
256
|
+
return entry;
|
|
257
|
+
}
|
|
258
|
+
const rawUrl = extractURL(entry);
|
|
259
|
+
if (!rawUrl) {
|
|
260
|
+
return entry;
|
|
261
|
+
}
|
|
262
|
+
const encodedUrl = safeEncodeURI(rawUrl);
|
|
263
|
+
if (cache.background.has(encodedUrl)) {
|
|
264
|
+
const dataUrl = cache.background.get(encodedUrl);
|
|
265
|
+
return dataUrl ? `url("${dataUrl}")` : "none";
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const dataUrl = await snapFetch(encodedUrl, { as: "dataURL", useProxy: options.useProxy });
|
|
269
|
+
if (dataUrl.ok) {
|
|
270
|
+
cache.background.set(encodedUrl, dataUrl.data);
|
|
271
|
+
return `url("${dataUrl.data}")`;
|
|
272
|
+
}
|
|
273
|
+
cache.background.set(encodedUrl, null);
|
|
274
|
+
return "none";
|
|
275
|
+
} catch {
|
|
276
|
+
cache.background.set(encodedUrl, null);
|
|
277
|
+
return "none";
|
|
278
|
+
}
|
|
33
279
|
}
|
|
34
280
|
|
|
35
|
-
// src/utils/
|
|
281
|
+
// src/utils/css.js
|
|
282
|
+
var NO_CAPTURE_TAGS = /* @__PURE__ */ new Set([
|
|
283
|
+
"meta",
|
|
284
|
+
"script",
|
|
285
|
+
"noscript",
|
|
286
|
+
"title",
|
|
287
|
+
"link",
|
|
288
|
+
"template"
|
|
289
|
+
]);
|
|
290
|
+
var NO_DEFAULTS_TAGS = /* @__PURE__ */ new Set([
|
|
291
|
+
// non-painting / head stuff
|
|
292
|
+
"meta",
|
|
293
|
+
"link",
|
|
294
|
+
"style",
|
|
295
|
+
"title",
|
|
296
|
+
"noscript",
|
|
297
|
+
"script",
|
|
298
|
+
"template",
|
|
299
|
+
// SVG whole namespace (safe for LeaderLine/presentation attrs)
|
|
300
|
+
"svg",
|
|
301
|
+
"g",
|
|
302
|
+
"defs",
|
|
303
|
+
"use",
|
|
304
|
+
"marker",
|
|
305
|
+
"mask",
|
|
306
|
+
"clipPath",
|
|
307
|
+
"pattern",
|
|
308
|
+
"path",
|
|
309
|
+
"polygon",
|
|
310
|
+
"polyline",
|
|
311
|
+
"line",
|
|
312
|
+
"circle",
|
|
313
|
+
"ellipse",
|
|
314
|
+
"rect",
|
|
315
|
+
"filter",
|
|
316
|
+
"lineargradient",
|
|
317
|
+
"radialgradient",
|
|
318
|
+
"stop"
|
|
319
|
+
]);
|
|
36
320
|
var commonTags = [
|
|
37
321
|
"div",
|
|
38
322
|
"span",
|
|
@@ -59,12 +343,6 @@ var commonTags = [
|
|
|
59
343
|
"h4",
|
|
60
344
|
"h5",
|
|
61
345
|
"h6",
|
|
62
|
-
"svg",
|
|
63
|
-
"path",
|
|
64
|
-
"circle",
|
|
65
|
-
"rect",
|
|
66
|
-
"line",
|
|
67
|
-
"g",
|
|
68
346
|
"table",
|
|
69
347
|
"thead",
|
|
70
348
|
"tbody",
|
|
@@ -74,28 +352,33 @@ var commonTags = [
|
|
|
74
352
|
];
|
|
75
353
|
function precacheCommonTags() {
|
|
76
354
|
for (let tag of commonTags) {
|
|
77
|
-
|
|
355
|
+
const t = String(tag).toLowerCase();
|
|
356
|
+
if (NO_CAPTURE_TAGS.has(t)) continue;
|
|
357
|
+
if (NO_DEFAULTS_TAGS.has(t)) continue;
|
|
358
|
+
getDefaultStyleForTag(t);
|
|
78
359
|
}
|
|
79
360
|
}
|
|
80
361
|
function getDefaultStyleForTag(tagName) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
|
|
85
|
-
if (skipTags.has(tagName)) {
|
|
362
|
+
tagName = String(tagName).toLowerCase();
|
|
363
|
+
if (NO_DEFAULTS_TAGS.has(tagName)) {
|
|
86
364
|
const empty = {};
|
|
87
365
|
cache.defaultStyle.set(tagName, empty);
|
|
88
366
|
return empty;
|
|
89
367
|
}
|
|
368
|
+
if (cache.defaultStyle.has(tagName)) {
|
|
369
|
+
return cache.defaultStyle.get(tagName);
|
|
370
|
+
}
|
|
90
371
|
let sandbox = document.getElementById("snapdom-sandbox");
|
|
91
372
|
if (!sandbox) {
|
|
92
373
|
sandbox = document.createElement("div");
|
|
93
374
|
sandbox.id = "snapdom-sandbox";
|
|
375
|
+
sandbox.setAttribute("data-snapdom-sandbox", "true");
|
|
376
|
+
sandbox.setAttribute("aria-hidden", "true");
|
|
94
377
|
sandbox.style.position = "absolute";
|
|
95
378
|
sandbox.style.left = "-9999px";
|
|
96
379
|
sandbox.style.top = "-9999px";
|
|
97
|
-
sandbox.style.width = "
|
|
98
|
-
sandbox.style.height = "
|
|
380
|
+
sandbox.style.width = "0px";
|
|
381
|
+
sandbox.style.height = "0px";
|
|
99
382
|
sandbox.style.overflow = "hidden";
|
|
100
383
|
document.body.appendChild(sandbox);
|
|
101
384
|
}
|
|
@@ -105,32 +388,55 @@ function getDefaultStyleForTag(tagName) {
|
|
|
105
388
|
const styles = getComputedStyle(el);
|
|
106
389
|
const defaults = {};
|
|
107
390
|
for (let prop of styles) {
|
|
108
|
-
|
|
391
|
+
if (shouldIgnoreProp(prop)) continue;
|
|
392
|
+
const value = styles.getPropertyValue(prop);
|
|
393
|
+
defaults[prop] = value;
|
|
109
394
|
}
|
|
110
395
|
sandbox.removeChild(el);
|
|
111
396
|
cache.defaultStyle.set(tagName, defaults);
|
|
112
397
|
return defaults;
|
|
113
398
|
}
|
|
114
|
-
var
|
|
115
|
-
|
|
399
|
+
var NO_PAINT_TOKEN = /(?:^|-)(animation|transition)(?:-|$)/i;
|
|
400
|
+
var NO_PAINT_PREFIX = /^(--|view-timeline|scroll-timeline|animation-trigger|offset-|position-try|app-region|interactivity|overlay|view-transition|-webkit-locale|-webkit-user-(?:drag|modify)|-webkit-tap-highlight-color|-webkit-text-security)$/i;
|
|
401
|
+
var NO_PAINT_EXACT = /* @__PURE__ */ new Set([
|
|
402
|
+
// Interaction hints
|
|
403
|
+
"cursor",
|
|
404
|
+
"pointer-events",
|
|
405
|
+
"touch-action",
|
|
406
|
+
"user-select",
|
|
407
|
+
// Printing/speech/reading-mode hints
|
|
408
|
+
"print-color-adjust",
|
|
409
|
+
"speak",
|
|
410
|
+
"reading-flow",
|
|
411
|
+
"reading-order",
|
|
412
|
+
// Anchoring/container/timeline scopes (metadata for layout queries)
|
|
413
|
+
"anchor-name",
|
|
414
|
+
"anchor-scope",
|
|
415
|
+
"container-name",
|
|
416
|
+
"container-type",
|
|
417
|
+
"timeline-scope"
|
|
116
418
|
]);
|
|
117
|
-
function
|
|
419
|
+
function shouldIgnoreProp(prop) {
|
|
420
|
+
const p = String(prop).toLowerCase();
|
|
421
|
+
if (NO_PAINT_EXACT.has(p)) return true;
|
|
422
|
+
if (NO_PAINT_PREFIX.test(p)) return true;
|
|
423
|
+
if (NO_PAINT_TOKEN.test(p)) return true;
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
function getStyleKey(snapshot, tagName) {
|
|
427
|
+
tagName = String(tagName || "").toLowerCase();
|
|
428
|
+
if (NO_DEFAULTS_TAGS.has(tagName)) {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
118
431
|
const entries = [];
|
|
119
|
-
const
|
|
432
|
+
const defaults = getDefaultStyleForTag(tagName);
|
|
120
433
|
for (let [prop, value] of Object.entries(snapshot)) {
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
entries.push(`${prop}:${value}`);
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
const defaultValue = defaultStyles[prop];
|
|
128
|
-
if (value && value !== defaultValue) {
|
|
129
|
-
entries.push(`${prop}:${value}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
434
|
+
if (shouldIgnoreProp(prop)) continue;
|
|
435
|
+
const def = defaults[prop];
|
|
436
|
+
if (value && value !== def) entries.push(`${prop}:${value}`);
|
|
132
437
|
}
|
|
133
|
-
|
|
438
|
+
entries.sort();
|
|
439
|
+
return entries.join(";");
|
|
134
440
|
}
|
|
135
441
|
function collectUsedTagNames(root) {
|
|
136
442
|
const tagSet = /* @__PURE__ */ new Set();
|
|
@@ -151,6 +457,7 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
151
457
|
const styles = cache.defaultStyle.get(tagName);
|
|
152
458
|
if (!styles) continue;
|
|
153
459
|
const key = Object.entries(styles).map(([k, v]) => `${k}:${v};`).sort().join("");
|
|
460
|
+
if (!key) continue;
|
|
154
461
|
if (!groups.has(key)) {
|
|
155
462
|
groups.set(key, []);
|
|
156
463
|
}
|
|
@@ -163,44 +470,13 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
163
470
|
}
|
|
164
471
|
return css;
|
|
165
472
|
}
|
|
166
|
-
function generateCSSClasses() {
|
|
167
|
-
const
|
|
473
|
+
function generateCSSClasses(styleMap) {
|
|
474
|
+
const keys = Array.from(new Set(styleMap.values())).filter(Boolean).sort();
|
|
168
475
|
const classMap = /* @__PURE__ */ new Map();
|
|
169
|
-
let
|
|
170
|
-
for (const
|
|
171
|
-
if (!key.trim()) continue;
|
|
172
|
-
classMap.set(key, `c${counter++}`);
|
|
173
|
-
}
|
|
476
|
+
let i = 1;
|
|
477
|
+
for (const k of keys) classMap.set(k, `c${i++}`);
|
|
174
478
|
return classMap;
|
|
175
479
|
}
|
|
176
|
-
|
|
177
|
-
// src/utils/helpers.js
|
|
178
|
-
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
179
|
-
const rawUrl = extractURL(entry);
|
|
180
|
-
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
181
|
-
if (rawUrl) {
|
|
182
|
-
const encodedUrl = safeEncodeURI(rawUrl);
|
|
183
|
-
if (cache.background.has(encodedUrl)) {
|
|
184
|
-
return options.skipInline ? void 0 : `url(${cache.background.get(encodedUrl)})`;
|
|
185
|
-
} else {
|
|
186
|
-
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
187
|
-
cache.background.set(encodedUrl, dataUrl);
|
|
188
|
-
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (isGradient || entry === "none") {
|
|
192
|
-
return entry;
|
|
193
|
-
}
|
|
194
|
-
return entry;
|
|
195
|
-
}
|
|
196
|
-
function idle(fn, { fast = false } = {}) {
|
|
197
|
-
if (fast) return fn();
|
|
198
|
-
if ("requestIdleCallback" in window) {
|
|
199
|
-
requestIdleCallback(fn, { timeout: 50 });
|
|
200
|
-
} else {
|
|
201
|
-
setTimeout(fn, 1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
480
|
function getStyle(el, pseudo = null) {
|
|
205
481
|
if (!(el instanceof Element)) {
|
|
206
482
|
return window.getComputedStyle(el, pseudo);
|
|
@@ -227,122 +503,6 @@ function parseContent(content) {
|
|
|
227
503
|
}
|
|
228
504
|
return clean;
|
|
229
505
|
}
|
|
230
|
-
function extractURL(value) {
|
|
231
|
-
const match = value.match(/url\((['"]?)(.*?)(\1)\)/);
|
|
232
|
-
if (!match) return null;
|
|
233
|
-
const url = match[2].trim();
|
|
234
|
-
if (url.startsWith("#")) return null;
|
|
235
|
-
return url;
|
|
236
|
-
}
|
|
237
|
-
function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
|
|
238
|
-
function getCrossOriginMode(url) {
|
|
239
|
-
try {
|
|
240
|
-
const parsed = new URL(url, window.location.href);
|
|
241
|
-
return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
|
|
242
|
-
} catch {
|
|
243
|
-
return "anonymous";
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async function fetchWithFallback(url) {
|
|
247
|
-
const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
|
|
248
|
-
mode: "cors",
|
|
249
|
-
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
250
|
-
}).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
|
|
251
|
-
const reader = new FileReader();
|
|
252
|
-
reader.onloadend = () => {
|
|
253
|
-
const base64 = reader.result;
|
|
254
|
-
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
255
|
-
reject(new Error("Invalid image data URL"));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
resolve(base64);
|
|
259
|
-
};
|
|
260
|
-
reader.onerror = () => reject(new Error("FileReader error"));
|
|
261
|
-
reader.readAsDataURL(blob);
|
|
262
|
-
}));
|
|
263
|
-
try {
|
|
264
|
-
return await fetchBlobAsDataURL(url);
|
|
265
|
-
} catch (e) {
|
|
266
|
-
if (useProxy && typeof useProxy === "string") {
|
|
267
|
-
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
268
|
-
try {
|
|
269
|
-
return await fetchBlobAsDataURL(proxied);
|
|
270
|
-
} catch {
|
|
271
|
-
throw new Error("[SnapDOM - fetchImage] CORS restrictions prevented image capture (even via proxy)");
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
throw new Error("[SnapDOM - fetchImage] Fetch fallback failed and no proxy provided");
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const crossOriginValue = getCrossOriginMode(src);
|
|
279
|
-
if (cache.image.has(src)) {
|
|
280
|
-
return Promise.resolve(cache.image.get(src));
|
|
281
|
-
}
|
|
282
|
-
const isDataURI = src.startsWith("data:image/");
|
|
283
|
-
if (isDataURI) {
|
|
284
|
-
cache.image.set(src, src);
|
|
285
|
-
return Promise.resolve(src);
|
|
286
|
-
}
|
|
287
|
-
const isSVG = /\.svg(\?.*)?$/i.test(src);
|
|
288
|
-
if (isSVG) {
|
|
289
|
-
return (async () => {
|
|
290
|
-
try {
|
|
291
|
-
const response = await fetch(src, {
|
|
292
|
-
mode: "cors",
|
|
293
|
-
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
294
|
-
});
|
|
295
|
-
const svgText = await response.text();
|
|
296
|
-
const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
|
297
|
-
cache.image.set(src, encoded);
|
|
298
|
-
return encoded;
|
|
299
|
-
} catch {
|
|
300
|
-
return fetchWithFallback(src);
|
|
301
|
-
}
|
|
302
|
-
})();
|
|
303
|
-
}
|
|
304
|
-
return new Promise((resolve, reject) => {
|
|
305
|
-
const timeoutId = setTimeout(() => {
|
|
306
|
-
reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
|
|
307
|
-
}, timeout);
|
|
308
|
-
const image = new Image();
|
|
309
|
-
image.crossOrigin = crossOriginValue;
|
|
310
|
-
image.onload = async () => {
|
|
311
|
-
clearTimeout(timeoutId);
|
|
312
|
-
try {
|
|
313
|
-
await image.decode();
|
|
314
|
-
const canvas = document.createElement("canvas");
|
|
315
|
-
canvas.width = image.width;
|
|
316
|
-
canvas.height = image.height;
|
|
317
|
-
const ctx = canvas.getContext("2d");
|
|
318
|
-
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
319
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
320
|
-
cache.image.set(src, dataURL);
|
|
321
|
-
resolve(dataURL);
|
|
322
|
-
} catch {
|
|
323
|
-
try {
|
|
324
|
-
const fallbackDataURL = await fetchWithFallback(src);
|
|
325
|
-
cache.image.set(src, fallbackDataURL);
|
|
326
|
-
resolve(fallbackDataURL);
|
|
327
|
-
} catch (e) {
|
|
328
|
-
reject(e);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
image.onerror = async () => {
|
|
333
|
-
clearTimeout(timeoutId);
|
|
334
|
-
console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
|
|
335
|
-
try {
|
|
336
|
-
const fallbackDataURL = await fetchWithFallback(src);
|
|
337
|
-
cache.image.set(src, fallbackDataURL);
|
|
338
|
-
resolve(fallbackDataURL);
|
|
339
|
-
} catch (e) {
|
|
340
|
-
reject(e);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
image.src = src;
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
506
|
function snapshotComputedStyle(style) {
|
|
347
507
|
const snap = {};
|
|
348
508
|
for (let prop of style) {
|
|
@@ -350,36 +510,6 @@ function snapshotComputedStyle(style) {
|
|
|
350
510
|
}
|
|
351
511
|
return snap;
|
|
352
512
|
}
|
|
353
|
-
function isSafari() {
|
|
354
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
355
|
-
}
|
|
356
|
-
function stripTranslate(transform) {
|
|
357
|
-
if (!transform || transform === "none") return "";
|
|
358
|
-
let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
|
|
359
|
-
cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
|
|
360
|
-
const parts = values.split(",").map((s) => s.trim());
|
|
361
|
-
if (parts.length !== 6) return `matrix(${values})`;
|
|
362
|
-
parts[4] = "0";
|
|
363
|
-
parts[5] = "0";
|
|
364
|
-
return `matrix(${parts.join(", ")})`;
|
|
365
|
-
});
|
|
366
|
-
cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
|
|
367
|
-
const parts = values.split(",").map((s) => s.trim());
|
|
368
|
-
if (parts.length !== 16) return `matrix3d(${values})`;
|
|
369
|
-
parts[12] = "0";
|
|
370
|
-
parts[13] = "0";
|
|
371
|
-
return `matrix3d(${parts.join(", ")})`;
|
|
372
|
-
});
|
|
373
|
-
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
374
|
-
}
|
|
375
|
-
function safeEncodeURI(uri) {
|
|
376
|
-
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
377
|
-
try {
|
|
378
|
-
return encodeURI(uri);
|
|
379
|
-
} catch {
|
|
380
|
-
return uri;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
513
|
function splitBackgroundImage(bg) {
|
|
384
514
|
const parts = [];
|
|
385
515
|
let depth = 0;
|
|
@@ -397,50 +527,272 @@ function splitBackgroundImage(bg) {
|
|
|
397
527
|
return parts;
|
|
398
528
|
}
|
|
399
529
|
|
|
530
|
+
// src/utils/browser.js
|
|
531
|
+
function idle(fn, { fast = false } = {}) {
|
|
532
|
+
if (fast) return fn();
|
|
533
|
+
if ("requestIdleCallback" in window) {
|
|
534
|
+
requestIdleCallback(fn, { timeout: 50 });
|
|
535
|
+
} else {
|
|
536
|
+
setTimeout(fn, 1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function isSafari() {
|
|
540
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
541
|
+
}
|
|
542
|
+
|
|
400
543
|
// src/modules/styles.js
|
|
544
|
+
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
545
|
+
var snapshotKeyCache = /* @__PURE__ */ new Map();
|
|
546
|
+
var __epoch = 0;
|
|
547
|
+
function bumpEpoch() {
|
|
548
|
+
__epoch++;
|
|
549
|
+
}
|
|
550
|
+
var __wired = false;
|
|
551
|
+
function setupInvalidationOnce(root = document.documentElement) {
|
|
552
|
+
if (__wired) return;
|
|
553
|
+
__wired = true;
|
|
554
|
+
try {
|
|
555
|
+
const domObs = new MutationObserver(() => bumpEpoch());
|
|
556
|
+
domObs.observe(root, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const headObs = new MutationObserver(() => bumpEpoch());
|
|
561
|
+
headObs.observe(document.head, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const f = document.fonts;
|
|
566
|
+
if (f) {
|
|
567
|
+
f.addEventListener?.("loadingdone", bumpEpoch);
|
|
568
|
+
f.ready?.then(() => bumpEpoch()).catch(() => {
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
401
574
|
function snapshotComputedStyleFull(style) {
|
|
402
|
-
const
|
|
403
|
-
const
|
|
575
|
+
const out = {};
|
|
576
|
+
const vis = style.getPropertyValue("visibility");
|
|
404
577
|
for (let i = 0; i < style.length; i++) {
|
|
405
578
|
const prop = style[i];
|
|
406
579
|
let val = style.getPropertyValue(prop);
|
|
407
580
|
if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
|
|
408
581
|
val = "none";
|
|
409
582
|
}
|
|
410
|
-
|
|
583
|
+
out[prop] = val;
|
|
411
584
|
}
|
|
412
|
-
if (
|
|
413
|
-
|
|
585
|
+
if (vis === "hidden") out.opacity = "0";
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
var __snapshotSig = /* @__PURE__ */ new WeakMap();
|
|
589
|
+
function styleSignature(snap) {
|
|
590
|
+
let sig = __snapshotSig.get(snap);
|
|
591
|
+
if (sig) return sig;
|
|
592
|
+
const entries = Object.entries(snap).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
593
|
+
sig = entries.map(([k, v]) => `${k}:${v}`).join(";");
|
|
594
|
+
__snapshotSig.set(snap, sig);
|
|
595
|
+
return sig;
|
|
596
|
+
}
|
|
597
|
+
function getSnapshot(el, preStyle = null) {
|
|
598
|
+
const rec = snapshotCache.get(el);
|
|
599
|
+
if (rec && rec.epoch === __epoch) return rec.snapshot;
|
|
600
|
+
const style = preStyle || getComputedStyle(el);
|
|
601
|
+
const snap = snapshotComputedStyleFull(style);
|
|
602
|
+
snapshotCache.set(el, { epoch: __epoch, snapshot: snap });
|
|
603
|
+
return snap;
|
|
604
|
+
}
|
|
605
|
+
function _resolveCtx(sessionOrCtx, opts) {
|
|
606
|
+
if (sessionOrCtx && sessionOrCtx.session && sessionOrCtx.persist) return sessionOrCtx;
|
|
607
|
+
if (sessionOrCtx && (sessionOrCtx.styleMap || sessionOrCtx.styleCache || sessionOrCtx.nodeMap)) {
|
|
608
|
+
return {
|
|
609
|
+
session: sessionOrCtx,
|
|
610
|
+
persist: {
|
|
611
|
+
snapshotKeyCache,
|
|
612
|
+
defaultStyle: cache.defaultStyle,
|
|
613
|
+
baseStyle: cache.baseStyle,
|
|
614
|
+
image: cache.image,
|
|
615
|
+
resource: cache.resource,
|
|
616
|
+
background: cache.background,
|
|
617
|
+
font: cache.font
|
|
618
|
+
},
|
|
619
|
+
options: opts || {}
|
|
620
|
+
};
|
|
414
621
|
}
|
|
415
|
-
return
|
|
622
|
+
return {
|
|
623
|
+
session: cache.session,
|
|
624
|
+
persist: {
|
|
625
|
+
snapshotKeyCache,
|
|
626
|
+
defaultStyle: cache.defaultStyle,
|
|
627
|
+
baseStyle: cache.baseStyle,
|
|
628
|
+
image: cache.image,
|
|
629
|
+
resource: cache.resource,
|
|
630
|
+
background: cache.background,
|
|
631
|
+
font: cache.font
|
|
632
|
+
},
|
|
633
|
+
options: sessionOrCtx || opts || {}
|
|
634
|
+
};
|
|
416
635
|
}
|
|
417
|
-
function inlineAllStyles(source, clone,
|
|
636
|
+
async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
418
637
|
if (source.tagName === "STYLE") return;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
638
|
+
const ctx = _resolveCtx(sessionOrCtx, opts);
|
|
639
|
+
const resetMode = ctx.options && ctx.options.cache || "auto";
|
|
640
|
+
if (resetMode !== "disabled") setupInvalidationOnce(document.documentElement);
|
|
641
|
+
if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
|
|
642
|
+
const author = source.getAttribute?.("style");
|
|
643
|
+
if (author) clone.setAttribute("style", author);
|
|
644
|
+
}
|
|
645
|
+
const { session, persist } = ctx;
|
|
646
|
+
if (!session.styleCache.has(source)) {
|
|
647
|
+
session.styleCache.set(source, getComputedStyle(source));
|
|
648
|
+
}
|
|
649
|
+
const pre = session.styleCache.get(source);
|
|
650
|
+
const snap = getSnapshot(source, pre);
|
|
651
|
+
const sig = styleSignature(snap);
|
|
652
|
+
let key = persist.snapshotKeyCache.get(sig);
|
|
653
|
+
if (!key) {
|
|
654
|
+
const tag = source.tagName?.toLowerCase() || "div";
|
|
655
|
+
key = getStyleKey(snap, tag);
|
|
656
|
+
persist.snapshotKeyCache.set(sig, key);
|
|
657
|
+
}
|
|
658
|
+
session.styleMap.set(clone, key);
|
|
437
659
|
}
|
|
438
660
|
|
|
439
661
|
// src/core/clone.js
|
|
440
|
-
function
|
|
662
|
+
function addNotSlottedRightmost(sel) {
|
|
663
|
+
sel = sel.trim();
|
|
664
|
+
if (!sel) return sel;
|
|
665
|
+
if (/\:not\(\s*\[data-sd-slotted\]\s*\)\s*$/.test(sel)) return sel;
|
|
666
|
+
return `${sel}:not([data-sd-slotted])`;
|
|
667
|
+
}
|
|
668
|
+
function wrapWithScope(selectorList, scopeSelector, excludeSlotted = true) {
|
|
669
|
+
return selectorList.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
670
|
+
if (s.startsWith(":where(")) return s;
|
|
671
|
+
if (s.startsWith("@")) return s;
|
|
672
|
+
const body = excludeSlotted ? addNotSlottedRightmost(s) : s;
|
|
673
|
+
return `:where(${scopeSelector} ${body})`;
|
|
674
|
+
}).join(", ");
|
|
675
|
+
}
|
|
676
|
+
function rewriteShadowCSS(cssText, scopeSelector) {
|
|
677
|
+
if (!cssText) return "";
|
|
678
|
+
cssText = cssText.replace(/:host\(([^)]+)\)/g, (_, sel) => {
|
|
679
|
+
return `:where(${scopeSelector}:is(${sel.trim()}))`;
|
|
680
|
+
});
|
|
681
|
+
cssText = cssText.replace(/:host\b/g, `:where(${scopeSelector})`);
|
|
682
|
+
cssText = cssText.replace(/:host-context\(([^)]+)\)/g, (_, sel) => {
|
|
683
|
+
return `:where(:where(${sel.trim()}) ${scopeSelector})`;
|
|
684
|
+
});
|
|
685
|
+
cssText = cssText.replace(/::slotted\(([^)]+)\)/g, (_, sel) => {
|
|
686
|
+
return `:where(${scopeSelector} ${sel.trim()})`;
|
|
687
|
+
});
|
|
688
|
+
cssText = cssText.replace(/(^|})(\s*)([^@}{]+){/g, (_, brace, ws, selectorList) => {
|
|
689
|
+
const wrapped = wrapWithScope(
|
|
690
|
+
selectorList,
|
|
691
|
+
scopeSelector,
|
|
692
|
+
/*excludeSlotted*/
|
|
693
|
+
true
|
|
694
|
+
);
|
|
695
|
+
return `${brace}${ws}${wrapped}{`;
|
|
696
|
+
});
|
|
697
|
+
return cssText;
|
|
698
|
+
}
|
|
699
|
+
function nextShadowScopeId(sessionCache) {
|
|
700
|
+
sessionCache.shadowScopeSeq = (sessionCache.shadowScopeSeq || 0) + 1;
|
|
701
|
+
return `s${sessionCache.shadowScopeSeq}`;
|
|
702
|
+
}
|
|
703
|
+
function extractShadowCSS(sr) {
|
|
704
|
+
let css = "";
|
|
705
|
+
try {
|
|
706
|
+
sr.querySelectorAll("style").forEach((s) => {
|
|
707
|
+
css += (s.textContent || "") + "\n";
|
|
708
|
+
});
|
|
709
|
+
const sheets = sr.adoptedStyleSheets || [];
|
|
710
|
+
for (const sh of sheets) {
|
|
711
|
+
try {
|
|
712
|
+
if (sh && sh.cssRules) {
|
|
713
|
+
for (const rule of sh.cssRules) css += rule.cssText + "\n";
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
return css;
|
|
721
|
+
}
|
|
722
|
+
function injectScopedStyle(hostClone, cssText, scopeId) {
|
|
723
|
+
if (!cssText) return;
|
|
724
|
+
const style = document.createElement("style");
|
|
725
|
+
style.setAttribute("data-sd", scopeId);
|
|
726
|
+
style.textContent = cssText;
|
|
727
|
+
hostClone.insertBefore(style, hostClone.firstChild || null);
|
|
728
|
+
}
|
|
729
|
+
function freezeImgSrcset(original, cloned) {
|
|
730
|
+
try {
|
|
731
|
+
const chosen = original.currentSrc || original.src || "";
|
|
732
|
+
if (!chosen) return;
|
|
733
|
+
cloned.setAttribute("src", chosen);
|
|
734
|
+
cloned.removeAttribute("srcset");
|
|
735
|
+
cloned.removeAttribute("sizes");
|
|
736
|
+
cloned.loading = "eager";
|
|
737
|
+
cloned.decoding = "sync";
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function collectCustomPropsFromCSS(cssText) {
|
|
742
|
+
const out = /* @__PURE__ */ new Set();
|
|
743
|
+
if (!cssText) return out;
|
|
744
|
+
const re = /var\(\s*(--[A-Za-z0-9_-]+)\b/g;
|
|
745
|
+
let m;
|
|
746
|
+
while (m = re.exec(cssText)) out.add(m[1]);
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
749
|
+
function resolveCustomProp(el, name) {
|
|
750
|
+
try {
|
|
751
|
+
const cs = getComputedStyle(el);
|
|
752
|
+
let v = cs.getPropertyValue(name).trim();
|
|
753
|
+
if (v) return v;
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
const rootCS = getComputedStyle(document.documentElement);
|
|
758
|
+
let v = rootCS.getPropertyValue(name).trim();
|
|
759
|
+
if (v) return v;
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
return "";
|
|
763
|
+
}
|
|
764
|
+
function buildSeedCustomPropsRule(hostEl, names, scopeSelector) {
|
|
765
|
+
const decls = [];
|
|
766
|
+
for (const name of names) {
|
|
767
|
+
const val = resolveCustomProp(hostEl, name);
|
|
768
|
+
if (val) decls.push(`${name}: ${val};`);
|
|
769
|
+
}
|
|
770
|
+
if (!decls.length) return "";
|
|
771
|
+
return `${scopeSelector}{${decls.join("")}}
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
function markSlottedSubtree(root) {
|
|
775
|
+
if (!root) return;
|
|
776
|
+
if (root.nodeType === Node.ELEMENT_NODE) {
|
|
777
|
+
root.setAttribute("data-sd-slotted", "");
|
|
778
|
+
}
|
|
779
|
+
if (root.querySelectorAll) {
|
|
780
|
+
root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function deepClone(node, sessionCache, options) {
|
|
441
784
|
if (!node) throw new Error("Invalid node");
|
|
442
785
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
443
786
|
let pendingSelectValue = null;
|
|
787
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
788
|
+
const tag = (node.localName || node.tagName || "").toLowerCase();
|
|
789
|
+
if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
if (NO_CAPTURE_TAGS.has(tag)) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
444
796
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
445
797
|
return node.cloneNode(true);
|
|
446
798
|
}
|
|
@@ -469,7 +821,7 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
469
821
|
}
|
|
470
822
|
if (typeof options.filter === "function") {
|
|
471
823
|
try {
|
|
472
|
-
if (!options.filter(node
|
|
824
|
+
if (!options.filter(node)) {
|
|
473
825
|
const spacer = document.createElement("div");
|
|
474
826
|
const rect = node.getBoundingClientRect();
|
|
475
827
|
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
|
|
@@ -486,8 +838,8 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
486
838
|
}
|
|
487
839
|
if (node.getAttribute("data-capture") === "placeholder") {
|
|
488
840
|
const clone2 = node.cloneNode(false);
|
|
489
|
-
|
|
490
|
-
inlineAllStyles(node, clone2,
|
|
841
|
+
sessionCache.nodeMap.set(clone2, node);
|
|
842
|
+
inlineAllStyles(node, clone2, sessionCache, options);
|
|
491
843
|
const placeholder = document.createElement("div");
|
|
492
844
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
493
845
|
placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
|
|
@@ -500,14 +852,17 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
500
852
|
img.src = dataURL;
|
|
501
853
|
img.width = node.width;
|
|
502
854
|
img.height = node.height;
|
|
503
|
-
|
|
504
|
-
inlineAllStyles(node, img,
|
|
855
|
+
sessionCache.nodeMap.set(img, node);
|
|
856
|
+
inlineAllStyles(node, img, sessionCache, options);
|
|
505
857
|
return img;
|
|
506
858
|
}
|
|
507
859
|
let clone;
|
|
508
860
|
try {
|
|
509
861
|
clone = node.cloneNode(false);
|
|
510
|
-
|
|
862
|
+
sessionCache.nodeMap.set(clone, node);
|
|
863
|
+
if (node.tagName === "IMG") {
|
|
864
|
+
freezeImgSrcset(node, clone);
|
|
865
|
+
}
|
|
511
866
|
} catch (err) {
|
|
512
867
|
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
513
868
|
throw err;
|
|
@@ -532,49 +887,56 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
532
887
|
if (node instanceof HTMLSelectElement) {
|
|
533
888
|
pendingSelectValue = node.value;
|
|
534
889
|
}
|
|
535
|
-
inlineAllStyles(node, clone,
|
|
890
|
+
inlineAllStyles(node, clone, sessionCache, options);
|
|
536
891
|
if (node.shadowRoot) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
for (const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
const shadowFrag = document.createDocumentFragment();
|
|
550
|
-
for (const child of node.shadowRoot.childNodes) {
|
|
551
|
-
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
552
|
-
const cssText = child.textContent || "";
|
|
553
|
-
if (cssText.trim() && compress) {
|
|
554
|
-
if (!cache.preStyle) cache.preStyle = /* @__PURE__ */ new WeakMap();
|
|
555
|
-
cache.preStyle.set(child, cssText);
|
|
556
|
-
}
|
|
557
|
-
continue;
|
|
892
|
+
try {
|
|
893
|
+
const slots = node.shadowRoot.querySelectorAll("slot");
|
|
894
|
+
for (const s of slots) {
|
|
895
|
+
let assigned = [];
|
|
896
|
+
try {
|
|
897
|
+
assigned = s.assignedNodes?.({ flatten: true }) || s.assignedNodes?.() || [];
|
|
898
|
+
} catch {
|
|
899
|
+
assigned = s.assignedNodes?.() || [];
|
|
558
900
|
}
|
|
559
|
-
|
|
560
|
-
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
901
|
+
for (const an of assigned) clonedAssignedNodes.add(an);
|
|
561
902
|
}
|
|
562
|
-
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
const scopeId = nextShadowScopeId(sessionCache);
|
|
906
|
+
const scopeSelector = `[data-sd="${scopeId}"]`;
|
|
907
|
+
try {
|
|
908
|
+
clone.setAttribute("data-sd", scopeId);
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
const rawCSS = extractShadowCSS(node.shadowRoot);
|
|
912
|
+
const rewritten = rewriteShadowCSS(rawCSS, scopeSelector);
|
|
913
|
+
const neededVars = collectCustomPropsFromCSS(rawCSS);
|
|
914
|
+
const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
|
|
915
|
+
injectScopedStyle(clone, seed + rewritten, scopeId);
|
|
916
|
+
const shadowFrag = document.createDocumentFragment();
|
|
917
|
+
for (const child of node.shadowRoot.childNodes) {
|
|
918
|
+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") continue;
|
|
919
|
+
const clonedChild = deepClone(child, sessionCache, options);
|
|
920
|
+
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
563
921
|
}
|
|
922
|
+
clone.appendChild(shadowFrag);
|
|
564
923
|
}
|
|
565
924
|
if (node.tagName === "SLOT") {
|
|
566
925
|
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
567
926
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
568
927
|
const fragment = document.createDocumentFragment();
|
|
569
928
|
for (const child of nodesToClone) {
|
|
570
|
-
const clonedChild = deepClone(child,
|
|
571
|
-
if (clonedChild)
|
|
929
|
+
const clonedChild = deepClone(child, sessionCache, options);
|
|
930
|
+
if (clonedChild) {
|
|
931
|
+
markSlottedSubtree(clonedChild);
|
|
932
|
+
fragment.appendChild(clonedChild);
|
|
933
|
+
}
|
|
572
934
|
}
|
|
573
935
|
return fragment;
|
|
574
936
|
}
|
|
575
937
|
for (const child of node.childNodes) {
|
|
576
938
|
if (clonedAssignedNodes.has(child)) continue;
|
|
577
|
-
const clonedChild = deepClone(child,
|
|
939
|
+
const clonedChild = deepClone(child, sessionCache, options);
|
|
578
940
|
if (clonedChild) clone.appendChild(clonedChild);
|
|
579
941
|
}
|
|
580
942
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
@@ -617,7 +979,7 @@ function extendIconFonts(fonts) {
|
|
|
617
979
|
}
|
|
618
980
|
}
|
|
619
981
|
}
|
|
620
|
-
function
|
|
982
|
+
function isIconFont2(input) {
|
|
621
983
|
const text = typeof input === "string" ? input : "";
|
|
622
984
|
const candidates = [...defaultIconFonts, ...userIconFonts];
|
|
623
985
|
for (const rx of candidates) {
|
|
@@ -664,190 +1026,620 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
664
1026
|
height
|
|
665
1027
|
};
|
|
666
1028
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1029
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
1030
|
+
"serif",
|
|
1031
|
+
"sans-serif",
|
|
1032
|
+
"monospace",
|
|
1033
|
+
"cursive",
|
|
1034
|
+
"fantasy",
|
|
1035
|
+
"system-ui",
|
|
1036
|
+
"emoji",
|
|
1037
|
+
"math",
|
|
1038
|
+
"fangsong",
|
|
1039
|
+
"ui-serif",
|
|
1040
|
+
"ui-sans-serif",
|
|
1041
|
+
"ui-monospace",
|
|
1042
|
+
"ui-rounded"
|
|
1043
|
+
]);
|
|
1044
|
+
function pickPrimaryFamily(familyList) {
|
|
1045
|
+
if (!familyList) return "";
|
|
1046
|
+
for (let raw of familyList.split(",")) {
|
|
1047
|
+
let f = raw.trim().replace(/^['"]+|['"]+$/g, "");
|
|
1048
|
+
if (!f) continue;
|
|
1049
|
+
if (!GENERIC_FAMILIES.has(f.toLowerCase())) return f;
|
|
1050
|
+
}
|
|
1051
|
+
return "";
|
|
1052
|
+
}
|
|
1053
|
+
function normWeight(w) {
|
|
1054
|
+
const t = String(w ?? "400").trim().toLowerCase();
|
|
1055
|
+
if (t === "normal") return 400;
|
|
1056
|
+
if (t === "bold") return 700;
|
|
1057
|
+
const n = parseInt(t, 10);
|
|
1058
|
+
return Number.isFinite(n) ? Math.min(900, Math.max(100, n)) : 400;
|
|
1059
|
+
}
|
|
1060
|
+
function normStyle(s) {
|
|
1061
|
+
const t = String(s ?? "normal").trim().toLowerCase();
|
|
1062
|
+
if (t.startsWith("italic")) return "italic";
|
|
1063
|
+
if (t.startsWith("oblique")) return "oblique";
|
|
1064
|
+
return "normal";
|
|
1065
|
+
}
|
|
1066
|
+
function normStretchPct(st) {
|
|
1067
|
+
const m = String(st ?? "100%").match(/(\d+(?:\.\d+)?)\s*%/);
|
|
1068
|
+
return m ? Math.max(50, Math.min(200, parseFloat(m[1]))) : 100;
|
|
1069
|
+
}
|
|
1070
|
+
function parseWeightSpec(spec) {
|
|
1071
|
+
const s = String(spec || "400").trim();
|
|
1072
|
+
const m = s.match(/^(\d{2,3})\s+(\d{2,3})$/);
|
|
1073
|
+
if (m) {
|
|
1074
|
+
const a = normWeight(m[1]), b = normWeight(m[2]);
|
|
1075
|
+
return { min: Math.min(a, b), max: Math.max(a, b) };
|
|
1076
|
+
}
|
|
1077
|
+
const v = normWeight(s);
|
|
1078
|
+
return { min: v, max: v };
|
|
1079
|
+
}
|
|
1080
|
+
function parseStyleSpec(spec) {
|
|
1081
|
+
const t = String(spec || "normal").trim().toLowerCase();
|
|
1082
|
+
if (t === "italic") return { kind: "italic" };
|
|
1083
|
+
if (t.startsWith("oblique")) return { kind: "oblique" };
|
|
1084
|
+
return { kind: "normal" };
|
|
681
1085
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1086
|
+
function parseStretchSpec(spec) {
|
|
1087
|
+
const s = String(spec || "100%").trim();
|
|
1088
|
+
const mm = s.match(/(\d+(?:\.\d+)?)\s*%\s+(\d+(?:\.\d+)?)\s*%/);
|
|
1089
|
+
if (mm) {
|
|
1090
|
+
const a = parseFloat(mm[1]), b = parseFloat(mm[2]);
|
|
1091
|
+
return { min: Math.min(a, b), max: Math.max(a, b) };
|
|
1092
|
+
}
|
|
1093
|
+
const m = s.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
1094
|
+
const v = m ? parseFloat(m[1]) : 100;
|
|
1095
|
+
return { min: v, max: v };
|
|
1096
|
+
}
|
|
1097
|
+
function isLikelyFontStylesheet(href, requiredFamilies) {
|
|
1098
|
+
if (!href) return false;
|
|
1099
|
+
try {
|
|
1100
|
+
const u = new URL(href, location.href);
|
|
1101
|
+
const sameOrigin = u.origin === location.origin;
|
|
1102
|
+
if (sameOrigin) return true;
|
|
1103
|
+
const host = u.host.toLowerCase();
|
|
1104
|
+
const FONT_HOSTS = [
|
|
1105
|
+
"fonts.googleapis.com",
|
|
1106
|
+
"fonts.gstatic.com",
|
|
1107
|
+
"use.typekit.net",
|
|
1108
|
+
"p.typekit.net",
|
|
1109
|
+
"kit.fontawesome.com",
|
|
1110
|
+
"use.fontawesome.com"
|
|
1111
|
+
];
|
|
1112
|
+
if (FONT_HOSTS.some((h) => host.endsWith(h))) return true;
|
|
1113
|
+
const path = (u.pathname + u.search).toLowerCase();
|
|
1114
|
+
if (/\bfont(s)?\b/.test(path) || /\.woff2?(\b|$)/.test(path)) return true;
|
|
1115
|
+
for (const fam of requiredFamilies) {
|
|
1116
|
+
const tokenA = fam.toLowerCase().replace(/\s+/g, "+");
|
|
1117
|
+
const tokenB = fam.toLowerCase().replace(/\s+/g, "-");
|
|
1118
|
+
if (path.includes(tokenA) || path.includes(tokenB)) return true;
|
|
1119
|
+
}
|
|
1120
|
+
return false;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function familiesFromRequired(required) {
|
|
1126
|
+
const out = /* @__PURE__ */ new Set();
|
|
1127
|
+
for (const k of required || []) {
|
|
1128
|
+
const fam = String(k).split("__")[0]?.trim();
|
|
1129
|
+
if (fam) out.add(fam);
|
|
1130
|
+
}
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
async function embedCustomFonts({
|
|
1134
|
+
required,
|
|
1135
|
+
usedCodepoints,
|
|
1136
|
+
exclude = void 0,
|
|
1137
|
+
localFonts = [],
|
|
1138
|
+
useProxy = ""
|
|
1139
|
+
} = {}) {
|
|
1140
|
+
if (!(required instanceof Set)) required = /* @__PURE__ */ new Set();
|
|
1141
|
+
if (!(usedCodepoints instanceof Set)) usedCodepoints = /* @__PURE__ */ new Set();
|
|
1142
|
+
const requiredIndex = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const key of required) {
|
|
1144
|
+
const [fam, w, s, st] = String(key).split("__");
|
|
1145
|
+
if (!fam) continue;
|
|
1146
|
+
const arr = requiredIndex.get(fam) || [];
|
|
1147
|
+
arr.push({ w: parseInt(w, 10), s, st: parseInt(st, 10) });
|
|
1148
|
+
requiredIndex.set(fam, arr);
|
|
1149
|
+
}
|
|
1150
|
+
const IMPORT_RE = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
1151
|
+
const URL_RE = /url\((["']?)([^"')]+)\1\)/g;
|
|
1152
|
+
const FACE_RE = /@font-face[^{}]*\{[^}]*\}/g;
|
|
1153
|
+
function faceMatchesRequired(fam, styleSpec, weightSpec, stretchSpec) {
|
|
1154
|
+
if (!requiredIndex.has(fam)) return false;
|
|
1155
|
+
const need = requiredIndex.get(fam);
|
|
1156
|
+
const ws = parseWeightSpec(weightSpec);
|
|
1157
|
+
const ss = parseStyleSpec(styleSpec);
|
|
1158
|
+
const ts = parseStretchSpec(stretchSpec);
|
|
1159
|
+
const faceIsRange = ws.min !== ws.max;
|
|
1160
|
+
const faceSingleW = ws.min;
|
|
1161
|
+
const styleOK = (reqKind) => ss.kind === "normal" && reqKind === "normal" || ss.kind !== "normal" && (reqKind === "italic" || reqKind === "oblique");
|
|
1162
|
+
let exactMatched = false;
|
|
1163
|
+
for (const r of need) {
|
|
1164
|
+
const wOk = faceIsRange ? r.w >= ws.min && r.w <= ws.max : r.w === faceSingleW;
|
|
1165
|
+
const sOk = styleOK(normStyle(r.s));
|
|
1166
|
+
const tOk = r.st >= ts.min && r.st <= ts.max;
|
|
1167
|
+
if (wOk && sOk && tOk) {
|
|
1168
|
+
exactMatched = true;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (exactMatched) return true;
|
|
1173
|
+
if (!faceIsRange) {
|
|
1174
|
+
for (const r of need) {
|
|
1175
|
+
const sOk = styleOK(normStyle(r.s));
|
|
1176
|
+
const tOk = r.st >= ts.min && r.st <= ts.max;
|
|
1177
|
+
const nearWeight = Math.abs(faceSingleW - r.w) <= 300;
|
|
1178
|
+
if (nearWeight && sOk && tOk) {
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
function parseUnicodeRange(ur) {
|
|
1186
|
+
if (!ur) return [];
|
|
1187
|
+
const ranges = [];
|
|
1188
|
+
const parts = ur.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1189
|
+
for (const p of parts) {
|
|
1190
|
+
const m = p.match(/^U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f?]+))?$/);
|
|
1191
|
+
if (!m) continue;
|
|
1192
|
+
const a = m[1], b = m[2];
|
|
1193
|
+
const expand = (hex) => {
|
|
1194
|
+
if (!hex.includes("?")) return parseInt(hex, 16);
|
|
1195
|
+
const min = parseInt(hex.replace(/\?/g, "0"), 16);
|
|
1196
|
+
const max = parseInt(hex.replace(/\?/g, "F"), 16);
|
|
1197
|
+
return [min, max];
|
|
1198
|
+
};
|
|
1199
|
+
if (b) {
|
|
1200
|
+
const A = expand(a), B = expand(b);
|
|
1201
|
+
const min = Array.isArray(A) ? A[0] : A;
|
|
1202
|
+
const max = Array.isArray(B) ? B[1] : B;
|
|
1203
|
+
ranges.push([Math.min(min, max), Math.max(min, max)]);
|
|
1204
|
+
} else {
|
|
1205
|
+
const X = expand(a);
|
|
1206
|
+
if (Array.isArray(X)) ranges.push([X[0], X[1]]);
|
|
1207
|
+
else ranges.push([X, X]);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return ranges;
|
|
1211
|
+
}
|
|
1212
|
+
function unicodeIntersects(used, ranges) {
|
|
1213
|
+
if (!ranges.length) return true;
|
|
1214
|
+
if (!used || used.size === 0) return true;
|
|
1215
|
+
for (const cp of used) {
|
|
1216
|
+
for (const [a, b] of ranges) if (cp >= a && cp <= b) return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
function extractSrcUrls(srcValue, baseHref) {
|
|
1221
|
+
const urls = [];
|
|
1222
|
+
if (!srcValue) return urls;
|
|
1223
|
+
for (const m of srcValue.matchAll(URL_RE)) {
|
|
1224
|
+
let u = (m[2] || "").trim();
|
|
1225
|
+
if (!u || u.startsWith("data:")) continue;
|
|
1226
|
+
if (!/^https?:/i.test(u)) {
|
|
1227
|
+
try {
|
|
1228
|
+
u = new URL(u, baseHref || location.href).href;
|
|
1229
|
+
} catch {
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
urls.push(u);
|
|
1233
|
+
}
|
|
1234
|
+
return urls;
|
|
1235
|
+
}
|
|
1236
|
+
async function inlineUrlsInCssBlock(cssBlock, baseHref) {
|
|
1237
|
+
let out = cssBlock;
|
|
1238
|
+
for (const m of cssBlock.matchAll(URL_RE)) {
|
|
1239
|
+
const raw = extractURL(m[0]);
|
|
1240
|
+
if (!raw) continue;
|
|
1241
|
+
let abs = raw;
|
|
1242
|
+
if (!abs.startsWith("http") && !abs.startsWith("data:")) {
|
|
1243
|
+
try {
|
|
1244
|
+
abs = new URL(abs, baseHref || location.href).href;
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (isIconFont2(abs)) continue;
|
|
1249
|
+
if (cache.resource?.has(abs)) {
|
|
1250
|
+
cache.font?.add(abs);
|
|
1251
|
+
out = out.replace(m[0], `url(${cache.resource.get(abs)})`);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (cache.font?.has(abs)) continue;
|
|
1255
|
+
try {
|
|
1256
|
+
const r = await snapFetch(abs, { as: "dataURL", useProxy, silent: true });
|
|
1257
|
+
if (r.ok && typeof r.data === "string") {
|
|
1258
|
+
const b64 = r.data;
|
|
1259
|
+
cache.resource?.set(abs, b64);
|
|
1260
|
+
cache.font?.add(abs);
|
|
1261
|
+
out = out.replace(m[0], `url(${b64})`);
|
|
1262
|
+
}
|
|
1263
|
+
} catch {
|
|
1264
|
+
console.warn("[snapDOM] Failed to fetch font resource:", abs);
|
|
1265
|
+
}
|
|
689
1266
|
}
|
|
690
|
-
return
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
function subsetFromRanges(ranges) {
|
|
1270
|
+
if (!ranges.length) return null;
|
|
1271
|
+
const hit = (a, b) => ranges.some(([x, y]) => !(y < a || x > b));
|
|
1272
|
+
const latin = hit(0, 255) || hit(305, 305);
|
|
1273
|
+
const latinExt = hit(256, 591) || hit(7680, 7935);
|
|
1274
|
+
const greek = hit(880, 1023);
|
|
1275
|
+
const cyr = hit(1024, 1279);
|
|
1276
|
+
const viet = hit(7840, 7929) || hit(258, 259) || hit(416, 417) || hit(431, 432);
|
|
1277
|
+
if (viet) return "vietnamese";
|
|
1278
|
+
if (cyr) return "cyrillic";
|
|
1279
|
+
if (greek) return "greek";
|
|
1280
|
+
if (latinExt) return "latin-ext";
|
|
1281
|
+
if (latin) return "latin";
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
function buildSimpleExcluder(ex = {}) {
|
|
1285
|
+
const famSet = new Set((ex.families || []).map((s) => String(s).toLowerCase()));
|
|
1286
|
+
const domSet = new Set((ex.domains || []).map((s) => String(s).toLowerCase()));
|
|
1287
|
+
const subSet = new Set((ex.subsets || []).map((s) => String(s).toLowerCase()));
|
|
1288
|
+
return (meta, parsedRanges) => {
|
|
1289
|
+
if (famSet.size && famSet.has(meta.family.toLowerCase())) return true;
|
|
1290
|
+
if (domSet.size) {
|
|
1291
|
+
for (const u of meta.srcUrls) {
|
|
1292
|
+
try {
|
|
1293
|
+
if (domSet.has(new URL(u).host.toLowerCase())) return true;
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (subSet.size) {
|
|
1299
|
+
const label = subsetFromRanges(parsedRanges);
|
|
1300
|
+
if (label && subSet.has(label)) return true;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
};
|
|
691
1304
|
}
|
|
692
|
-
const
|
|
693
|
-
|
|
1305
|
+
const simpleExcluder = buildSimpleExcluder(exclude);
|
|
1306
|
+
function dedupeFontFaces(cssText) {
|
|
1307
|
+
if (!cssText) return cssText;
|
|
1308
|
+
const FACE_RE2 = /@font-face[^{}]*\{[^}]*\}/gi;
|
|
1309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1310
|
+
const out = [];
|
|
1311
|
+
for (const block of cssText.match(FACE_RE2) || []) {
|
|
1312
|
+
const familyRaw = block.match(/font-family:\s*([^;]+);/i)?.[1] || "";
|
|
1313
|
+
const family = pickPrimaryFamily(familyRaw);
|
|
1314
|
+
const weightSpec = (block.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
|
|
1315
|
+
const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1316
|
+
const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1317
|
+
const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1318
|
+
const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1319
|
+
const urls = extractSrcUrls(srcRaw, location.href);
|
|
1320
|
+
const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
|
|
1321
|
+
const key = [
|
|
1322
|
+
String(family || "").toLowerCase(),
|
|
1323
|
+
weightSpec,
|
|
1324
|
+
styleSpec,
|
|
1325
|
+
stretchSpec,
|
|
1326
|
+
urange.toLowerCase(),
|
|
1327
|
+
srcPart
|
|
1328
|
+
].join("|");
|
|
1329
|
+
if (!seen.has(key)) {
|
|
1330
|
+
seen.add(key);
|
|
1331
|
+
out.push(block);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (out.length === 0) return cssText;
|
|
1335
|
+
let i = 0;
|
|
1336
|
+
return cssText.replace(FACE_RE2, () => out[i++] || "");
|
|
1337
|
+
}
|
|
1338
|
+
function buildFontsCacheKey() {
|
|
1339
|
+
const req = Array.from(required || []).sort().join("|");
|
|
1340
|
+
const ex = exclude ? JSON.stringify({
|
|
1341
|
+
families: (exclude.families || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1342
|
+
domains: (exclude.domains || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1343
|
+
subsets: (exclude.subsets || []).map((s) => String(s).toLowerCase()).sort()
|
|
1344
|
+
}) : "";
|
|
1345
|
+
const lf = (localFonts || []).map((f) => `${(f.family || "").toLowerCase()}::${f.weight || "normal"}::${f.style || "normal"}::${f.src || ""}`).sort().join("|");
|
|
1346
|
+
const px = useProxy || "";
|
|
1347
|
+
return `fonts-embed-css::req=${req}::ex=${ex}::lf=${lf}::px=${px}`;
|
|
1348
|
+
}
|
|
1349
|
+
const cacheKey = buildFontsCacheKey();
|
|
1350
|
+
if (cache.resource?.has(cacheKey)) {
|
|
1351
|
+
const css = cache.resource.get(cacheKey);
|
|
1352
|
+
return css;
|
|
1353
|
+
}
|
|
1354
|
+
const requiredFamilies = familiesFromRequired(required);
|
|
1355
|
+
const importUrls = [];
|
|
694
1356
|
for (const styleTag of document.querySelectorAll("style")) {
|
|
695
1357
|
const cssText = styleTag.textContent || "";
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (!
|
|
701
|
-
styleImports.push(importUrl);
|
|
702
|
-
}
|
|
1358
|
+
for (const m of cssText.matchAll(IMPORT_RE)) {
|
|
1359
|
+
const u = m[1];
|
|
1360
|
+
if (!u || isIconFont2(u)) continue;
|
|
1361
|
+
const hasLink = !!document.querySelector(`link[rel="stylesheet"][href="${u}"]`);
|
|
1362
|
+
if (!hasLink) importUrls.push(u);
|
|
703
1363
|
}
|
|
704
1364
|
}
|
|
705
|
-
|
|
706
|
-
|
|
1365
|
+
if (importUrls.length) {
|
|
1366
|
+
await Promise.all(importUrls.map((u) => new Promise((resolve) => {
|
|
1367
|
+
if (document.querySelector(`link[rel="stylesheet"][href="${u}"]`)) return resolve(null);
|
|
1368
|
+
const link = document.createElement("link");
|
|
1369
|
+
link.rel = "stylesheet";
|
|
1370
|
+
link.href = u;
|
|
1371
|
+
link.setAttribute("data-snapdom", "injected-import");
|
|
1372
|
+
link.onload = () => resolve(link);
|
|
1373
|
+
link.onerror = () => resolve(null);
|
|
1374
|
+
document.head.appendChild(link);
|
|
1375
|
+
})));
|
|
1376
|
+
}
|
|
707
1377
|
let finalCSS = "";
|
|
708
|
-
|
|
1378
|
+
const linkNodes = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((l) => !!l.href);
|
|
1379
|
+
for (const link of linkNodes) {
|
|
709
1380
|
try {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (cache.resource.has(url)) {
|
|
724
|
-
cache.font.add(url);
|
|
725
|
-
return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
|
|
726
|
-
}
|
|
727
|
-
if (cache.font.has(url)) return null;
|
|
1381
|
+
if (isIconFont2(link.href)) continue;
|
|
1382
|
+
let cssText = "";
|
|
1383
|
+
let sameOrigin = false;
|
|
1384
|
+
try {
|
|
1385
|
+
sameOrigin = new URL(link.href, location.href).origin === location.origin;
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1388
|
+
if (!sameOrigin) {
|
|
1389
|
+
if (!isLikelyFontStylesheet(link.href, requiredFamilies)) continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (sameOrigin) {
|
|
1392
|
+
const sheet = Array.from(document.styleSheets).find((s) => s.href === link.href);
|
|
1393
|
+
if (sheet) {
|
|
728
1394
|
try {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const reader = new FileReader();
|
|
733
|
-
reader.onload = () => resolve(reader.result);
|
|
734
|
-
reader.readAsDataURL(blob);
|
|
735
|
-
});
|
|
736
|
-
cache.resource.set(url, b64);
|
|
737
|
-
cache.font.add(url);
|
|
738
|
-
return { original: match[0], inlined: `url(${b64})` };
|
|
739
|
-
} catch (e) {
|
|
740
|
-
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
741
|
-
return null;
|
|
1395
|
+
const rules = sheet.cssRules || [];
|
|
1396
|
+
cssText = Array.from(rules).map((r) => r.cssText).join("");
|
|
1397
|
+
} catch {
|
|
742
1398
|
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (!cssText) {
|
|
1402
|
+
const res = await snapFetch(link.href, { as: "text", useProxy });
|
|
1403
|
+
cssText = res.data;
|
|
1404
|
+
if (isIconFont2(link.href)) continue;
|
|
1405
|
+
}
|
|
1406
|
+
let facesOut = "";
|
|
1407
|
+
for (const face of cssText.match(FACE_RE) || []) {
|
|
1408
|
+
const famRaw = (face.match(/font-family:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1409
|
+
const family = pickPrimaryFamily(famRaw);
|
|
1410
|
+
if (!family || isIconFont2(family)) {
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
const weightSpec = (face.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
|
|
1414
|
+
const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1415
|
+
const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1416
|
+
const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1417
|
+
const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1418
|
+
const srcUrls = extractSrcUrls(srcRaw, link.href);
|
|
1419
|
+
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const ranges = parseUnicodeRange(urange);
|
|
1423
|
+
if (!unicodeIntersects(usedCodepoints, ranges)) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: link.href };
|
|
1427
|
+
if (exclude && simpleExcluder(meta, ranges)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href) : face;
|
|
1431
|
+
facesOut += newFace + "";
|
|
748
1432
|
}
|
|
749
|
-
finalCSS +=
|
|
1433
|
+
if (facesOut.trim()) finalCSS += facesOut + "";
|
|
750
1434
|
} catch (e) {
|
|
751
|
-
console.warn("[snapdom] Failed to
|
|
1435
|
+
console.warn("[snapdom] Failed to process stylesheet:", link.href);
|
|
752
1436
|
}
|
|
753
1437
|
}
|
|
754
1438
|
for (const sheet of document.styleSheets) {
|
|
755
1439
|
try {
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1440
|
+
if (sheet.href && linkNodes.some((l) => l.href === sheet.href)) continue;
|
|
1441
|
+
const rules = sheet.cssRules || [];
|
|
1442
|
+
for (const rule of rules) {
|
|
1443
|
+
if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
|
|
1444
|
+
const famRaw = (rule.style.getPropertyValue("font-family") || "").trim();
|
|
1445
|
+
const family = pickPrimaryFamily(famRaw);
|
|
1446
|
+
if (!family || isIconFont2(family)) continue;
|
|
1447
|
+
const weightSpec = (rule.style.getPropertyValue("font-weight") || "400").trim();
|
|
1448
|
+
const styleSpec = (rule.style.getPropertyValue("font-style") || "normal").trim();
|
|
1449
|
+
const stretchSpec = (rule.style.getPropertyValue("font-stretch") || "100%").trim();
|
|
1450
|
+
const srcRaw = (rule.style.getPropertyValue("src") || "").trim();
|
|
1451
|
+
const urange = (rule.style.getPropertyValue("unicode-range") || "").trim();
|
|
1452
|
+
const srcUrls = extractSrcUrls(srcRaw, sheet.href || location.href);
|
|
1453
|
+
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
|
|
1454
|
+
const ranges = parseUnicodeRange(urange);
|
|
1455
|
+
if (!unicodeIntersects(usedCodepoints, ranges)) continue;
|
|
1456
|
+
const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: sheet.href || location.href };
|
|
1457
|
+
if (exclude && simpleExcluder(meta, ranges)) continue;
|
|
1458
|
+
if (/url\(/i.test(srcRaw)) {
|
|
1459
|
+
const inlinedSrc = await inlineUrlsInCssBlock(srcRaw, sheet.href || location.href);
|
|
1460
|
+
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
|
|
1461
|
+
} else {
|
|
1462
|
+
finalCSS += `@font-face{font-family:${family};src:${srcRaw};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
} catch {
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
for (const f of document.fonts || []) {
|
|
1470
|
+
if (!f || !f.family || f.status !== "loaded" || !f._snapdomSrc) continue;
|
|
1471
|
+
const fam = String(f.family).replace(/^['"]+|['"]+$/g, "");
|
|
1472
|
+
if (isIconFont2(fam)) continue;
|
|
1473
|
+
if (!requiredIndex.has(fam)) continue;
|
|
1474
|
+
if (exclude?.families && exclude.families.some((n) => String(n).toLowerCase() === fam.toLowerCase())) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
let b64 = f._snapdomSrc;
|
|
1478
|
+
if (!String(b64).startsWith("data:")) {
|
|
1479
|
+
if (cache.resource?.has(f._snapdomSrc)) {
|
|
1480
|
+
b64 = cache.resource.get(f._snapdomSrc);
|
|
1481
|
+
cache.font?.add(f._snapdomSrc);
|
|
1482
|
+
} else if (!cache.font?.has(f._snapdomSrc)) {
|
|
1483
|
+
try {
|
|
1484
|
+
const r = await snapFetch(f._snapdomSrc, { as: "dataURL", useProxy, silent: true });
|
|
1485
|
+
if (r.ok && typeof r.data === "string") {
|
|
1486
|
+
b64 = r.data;
|
|
1487
|
+
cache.resource?.set(f._snapdomSrc, b64);
|
|
1488
|
+
cache.font?.add(f._snapdomSrc);
|
|
1489
|
+
} else {
|
|
768
1490
|
continue;
|
|
769
1491
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
let rawUrl = match[2].trim();
|
|
774
|
-
if (!rawUrl) continue;
|
|
775
|
-
let url = rawUrl;
|
|
776
|
-
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
777
|
-
url = new URL(url, sheet.href || location.href).href;
|
|
778
|
-
}
|
|
779
|
-
if (isIconFont(url)) continue;
|
|
780
|
-
if (cache.resource.has(url)) {
|
|
781
|
-
cache.font.add(url);
|
|
782
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
if (cache.font.has(url)) continue;
|
|
786
|
-
try {
|
|
787
|
-
const res = await fetch(url);
|
|
788
|
-
const blob = await res.blob();
|
|
789
|
-
const b64 = await new Promise((resolve) => {
|
|
790
|
-
const reader = new FileReader();
|
|
791
|
-
reader.onload = () => resolve(reader.result);
|
|
792
|
-
reader.readAsDataURL(blob);
|
|
793
|
-
});
|
|
794
|
-
cache.resource.set(url, b64);
|
|
795
|
-
cache.font.add(url);
|
|
796
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
797
|
-
} catch (e) {
|
|
798
|
-
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
|
|
1492
|
+
} catch {
|
|
1493
|
+
console.warn("[snapdom] Failed to fetch dynamic font src:", f._snapdomSrc);
|
|
1494
|
+
continue;
|
|
802
1495
|
}
|
|
803
1496
|
}
|
|
804
1497
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
for (const font of
|
|
810
|
-
if (font
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1498
|
+
finalCSS += `@font-face{font-family:'${fam}';src:url(${b64});font-style:${f.style || "normal"};font-weight:${f.weight || "normal"};}`;
|
|
1499
|
+
}
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
for (const font of localFonts) {
|
|
1503
|
+
if (!font || typeof font !== "object") continue;
|
|
1504
|
+
const family = String(font.family || "").replace(/^['"]+|['"]+$/g, "");
|
|
1505
|
+
if (!family || isIconFont2(family)) continue;
|
|
1506
|
+
if (!requiredIndex.has(family)) continue;
|
|
1507
|
+
if (exclude?.families && exclude.families.some((n) => String(n).toLowerCase() === family.toLowerCase())) continue;
|
|
1508
|
+
const weight = font.weight != null ? String(font.weight) : "normal";
|
|
1509
|
+
const style = font.style != null ? String(font.style) : "normal";
|
|
1510
|
+
const stretch = font.stretchPct != null ? `${font.stretchPct}%` : "100%";
|
|
1511
|
+
const src = String(font.src || "");
|
|
1512
|
+
let b64 = src;
|
|
1513
|
+
if (!b64.startsWith("data:")) {
|
|
1514
|
+
if (cache.resource?.has(src)) {
|
|
1515
|
+
b64 = cache.resource.get(src);
|
|
1516
|
+
cache.font?.add(src);
|
|
1517
|
+
} else if (!cache.font?.has(src)) {
|
|
1518
|
+
try {
|
|
1519
|
+
const r = await snapFetch(src, { as: "dataURL", useProxy, silent: true });
|
|
1520
|
+
if (r.ok && typeof r.data === "string") {
|
|
1521
|
+
b64 = r.data;
|
|
1522
|
+
cache.resource?.set(src, b64);
|
|
1523
|
+
cache.font?.add(src);
|
|
1524
|
+
} else {
|
|
830
1525
|
continue;
|
|
831
1526
|
}
|
|
1527
|
+
} catch {
|
|
1528
|
+
console.warn("[snapdom] Failed to fetch localFonts src:", src);
|
|
1529
|
+
continue;
|
|
832
1530
|
}
|
|
833
1531
|
}
|
|
834
|
-
finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
|
|
835
1532
|
}
|
|
1533
|
+
finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};font-stretch:${stretch};}`;
|
|
836
1534
|
}
|
|
837
1535
|
if (finalCSS) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const style = document.createElement("style");
|
|
841
|
-
style.setAttribute("data-snapdom", "embedFonts");
|
|
842
|
-
style.textContent = finalCSS;
|
|
843
|
-
document.head.appendChild(style);
|
|
844
|
-
}
|
|
1536
|
+
finalCSS = dedupeFontFaces(finalCSS);
|
|
1537
|
+
cache.resource?.set(cacheKey, finalCSS);
|
|
845
1538
|
}
|
|
846
1539
|
return finalCSS;
|
|
847
1540
|
}
|
|
1541
|
+
function collectUsedFontVariants(root) {
|
|
1542
|
+
const req = /* @__PURE__ */ new Set();
|
|
1543
|
+
if (!root) return req;
|
|
1544
|
+
const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
|
|
1545
|
+
const addFromStyle = (cs) => {
|
|
1546
|
+
const family = pickPrimaryFamily(cs.fontFamily);
|
|
1547
|
+
if (!family) return;
|
|
1548
|
+
const key = (w, s, st) => `${family}__${normWeight(w)}__${normStyle(s)}__${normStretchPct(st)}`;
|
|
1549
|
+
req.add(key(cs.fontWeight, cs.fontStyle, cs.fontStretch));
|
|
1550
|
+
};
|
|
1551
|
+
addFromStyle(getComputedStyle(root));
|
|
1552
|
+
const csBeforeRoot = getComputedStyle(root, "::before");
|
|
1553
|
+
if (csBeforeRoot && csBeforeRoot.content && csBeforeRoot.content !== "none") addFromStyle(csBeforeRoot);
|
|
1554
|
+
const csAfterRoot = getComputedStyle(root, "::after");
|
|
1555
|
+
if (csAfterRoot && csAfterRoot.content && csAfterRoot.content !== "none") addFromStyle(csAfterRoot);
|
|
1556
|
+
while (tw.nextNode()) {
|
|
1557
|
+
const el = (
|
|
1558
|
+
/** @type {Element} */
|
|
1559
|
+
tw.currentNode
|
|
1560
|
+
);
|
|
1561
|
+
const cs = getComputedStyle(el);
|
|
1562
|
+
addFromStyle(cs);
|
|
1563
|
+
const b = getComputedStyle(el, "::before");
|
|
1564
|
+
if (b && b.content && b.content !== "none") addFromStyle(b);
|
|
1565
|
+
const a = getComputedStyle(el, "::after");
|
|
1566
|
+
if (a && a.content && a.content !== "none") addFromStyle(a);
|
|
1567
|
+
}
|
|
1568
|
+
return req;
|
|
1569
|
+
}
|
|
1570
|
+
function collectUsedCodepoints(root) {
|
|
1571
|
+
const used = /* @__PURE__ */ new Set();
|
|
1572
|
+
const pushText = (txt) => {
|
|
1573
|
+
if (!txt) return;
|
|
1574
|
+
for (const ch of txt) used.add(ch.codePointAt(0));
|
|
1575
|
+
};
|
|
1576
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null);
|
|
1577
|
+
while (walker.nextNode()) {
|
|
1578
|
+
const n = walker.currentNode;
|
|
1579
|
+
if (n.nodeType === Node.TEXT_NODE) {
|
|
1580
|
+
pushText(n.nodeValue || "");
|
|
1581
|
+
} else if (n.nodeType === Node.ELEMENT_NODE) {
|
|
1582
|
+
const el = (
|
|
1583
|
+
/** @type {Element} */
|
|
1584
|
+
n
|
|
1585
|
+
);
|
|
1586
|
+
for (const pseudo of ["::before", "::after"]) {
|
|
1587
|
+
const cs = getComputedStyle(el, pseudo);
|
|
1588
|
+
const c = cs?.getPropertyValue("content");
|
|
1589
|
+
if (!c || c === "none") continue;
|
|
1590
|
+
if (/^"/.test(c) || /^'/.test(c)) {
|
|
1591
|
+
pushText(c.slice(1, -1));
|
|
1592
|
+
} else {
|
|
1593
|
+
const matches = c.match(/\\[0-9A-Fa-f]{1,6}/g);
|
|
1594
|
+
if (matches) {
|
|
1595
|
+
for (const m of matches) {
|
|
1596
|
+
try {
|
|
1597
|
+
used.add(parseInt(m.slice(1), 16));
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return used;
|
|
1607
|
+
}
|
|
1608
|
+
async function ensureFontsReady(families, warmupRepetitions = 1) {
|
|
1609
|
+
try {
|
|
1610
|
+
await document.fonts.ready;
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
const fams = Array.from(families || []).filter(Boolean);
|
|
1614
|
+
if (fams.length === 0) return;
|
|
1615
|
+
const warmupOnce = () => {
|
|
1616
|
+
const container = document.createElement("div");
|
|
1617
|
+
container.style.cssText = `position: absolute !important;left: -9999px !important;top: 0 !important;opacity: 0 !important;pointer-events: none !important;contain: layout size style;`;
|
|
1618
|
+
for (const fam of fams) {
|
|
1619
|
+
const span = document.createElement("span");
|
|
1620
|
+
span.textContent = "AaBbGg1234\xC1\xC9\xCD\xD3\xDA\xE7\xF1\u2014\u221E";
|
|
1621
|
+
span.style.fontFamily = `"${fam}"`;
|
|
1622
|
+
span.style.fontWeight = "700";
|
|
1623
|
+
span.style.fontStyle = "italic";
|
|
1624
|
+
span.style.fontSize = "32px";
|
|
1625
|
+
span.style.lineHeight = "1";
|
|
1626
|
+
span.style.whiteSpace = "nowrap";
|
|
1627
|
+
span.style.margin = "0";
|
|
1628
|
+
span.style.padding = "0";
|
|
1629
|
+
container.appendChild(span);
|
|
1630
|
+
}
|
|
1631
|
+
document.body.appendChild(container);
|
|
1632
|
+
container.offsetWidth;
|
|
1633
|
+
document.body.removeChild(container);
|
|
1634
|
+
};
|
|
1635
|
+
for (let i = 0; i < Math.max(1, warmupRepetitions); i++) {
|
|
1636
|
+
warmupOnce();
|
|
1637
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
848
1640
|
|
|
849
1641
|
// src/modules/pseudo.js
|
|
850
|
-
async function inlinePseudoElements(source, clone, options) {
|
|
1642
|
+
async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
851
1643
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
852
1644
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
853
1645
|
try {
|
|
@@ -872,8 +1664,8 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
872
1664
|
span.textContent = first;
|
|
873
1665
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
874
1666
|
const snapshot2 = snapshotComputedStyle(style);
|
|
875
|
-
const key2 = getStyleKey(snapshot2, "span"
|
|
876
|
-
|
|
1667
|
+
const key2 = getStyleKey(snapshot2, "span");
|
|
1668
|
+
sessionCache.styleMap.set(span, key2);
|
|
877
1669
|
const restNode = document.createTextNode(rest);
|
|
878
1670
|
clone.replaceChild(restNode, textNode);
|
|
879
1671
|
clone.insertBefore(span, restNode);
|
|
@@ -893,7 +1685,7 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
893
1685
|
const borderStyle = style.borderStyle;
|
|
894
1686
|
const borderWidth = parseFloat(style.borderWidth);
|
|
895
1687
|
const transform = style.transform;
|
|
896
|
-
const
|
|
1688
|
+
const isIconFont22 = isIconFont2(fontFamily);
|
|
897
1689
|
const hasExplicitContent = content !== "none" && cleanContent !== "";
|
|
898
1690
|
const hasBg = bg && bg !== "none";
|
|
899
1691
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
@@ -906,9 +1698,9 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
906
1698
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
907
1699
|
pseudoEl.style.verticalAlign = "middle";
|
|
908
1700
|
const snapshot = snapshotComputedStyle(style);
|
|
909
|
-
const key = getStyleKey(snapshot, "span"
|
|
910
|
-
|
|
911
|
-
if (
|
|
1701
|
+
const key = getStyleKey(snapshot, "span");
|
|
1702
|
+
sessionCache.styleMap.set(pseudoEl, key);
|
|
1703
|
+
if (isIconFont22 && cleanContent.length === 1) {
|
|
912
1704
|
const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
913
1705
|
const imgEl = document.createElement("img");
|
|
914
1706
|
imgEl.src = dataUrl;
|
|
@@ -920,15 +1712,15 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
920
1712
|
if (rawUrl?.trim()) {
|
|
921
1713
|
try {
|
|
922
1714
|
const imgEl = document.createElement("img");
|
|
923
|
-
const dataUrl = await
|
|
924
|
-
imgEl.src = dataUrl;
|
|
1715
|
+
const dataUrl = await snapFetch(safeEncodeURI(rawUrl), { as: "dataURL", useProxy: options.useProxy });
|
|
1716
|
+
imgEl.src = dataUrl.data;
|
|
925
1717
|
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
926
1718
|
pseudoEl.appendChild(imgEl);
|
|
927
1719
|
} catch (e) {
|
|
928
1720
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
929
1721
|
}
|
|
930
1722
|
}
|
|
931
|
-
} else if (!
|
|
1723
|
+
} else if (!isIconFont22 && hasExplicitContent) {
|
|
932
1724
|
pseudoEl.textContent = cleanContent;
|
|
933
1725
|
}
|
|
934
1726
|
if (hasBg) {
|
|
@@ -956,7 +1748,7 @@ async function inlinePseudoElements(source, clone, options) {
|
|
|
956
1748
|
const sChildren = Array.from(source.children);
|
|
957
1749
|
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
958
1750
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
959
|
-
await inlinePseudoElements(sChildren[i], cChildren[i], options);
|
|
1751
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], sessionCache, options);
|
|
960
1752
|
}
|
|
961
1753
|
}
|
|
962
1754
|
|
|
@@ -1011,9 +1803,15 @@ function inlineExternalDefsAndSymbols(rootElement) {
|
|
|
1011
1803
|
}
|
|
1012
1804
|
|
|
1013
1805
|
// src/core/prepare.js
|
|
1014
|
-
async function prepareClone(element,
|
|
1806
|
+
async function prepareClone(element, options = {}) {
|
|
1807
|
+
const sessionCache = {
|
|
1808
|
+
styleMap: cache.session.styleMap,
|
|
1809
|
+
styleCache: cache.session.styleCache,
|
|
1810
|
+
nodeMap: cache.session.nodeMap
|
|
1811
|
+
};
|
|
1015
1812
|
let clone;
|
|
1016
1813
|
let classCSS = "";
|
|
1814
|
+
let shadowScopedCSS = "";
|
|
1017
1815
|
stabilizeLayout(element);
|
|
1018
1816
|
try {
|
|
1019
1817
|
inlineExternalDefsAndSymbols(element);
|
|
@@ -1021,42 +1819,45 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1021
1819
|
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1022
1820
|
}
|
|
1023
1821
|
try {
|
|
1024
|
-
clone = deepClone(element,
|
|
1822
|
+
clone = deepClone(element, sessionCache, options, element);
|
|
1025
1823
|
} catch (e) {
|
|
1026
1824
|
console.warn("deepClone failed:", e);
|
|
1027
1825
|
throw e;
|
|
1028
1826
|
}
|
|
1029
1827
|
try {
|
|
1030
|
-
await inlinePseudoElements(element, clone,
|
|
1828
|
+
await inlinePseudoElements(element, clone, sessionCache, options);
|
|
1031
1829
|
} catch (e) {
|
|
1032
1830
|
console.warn("inlinePseudoElements failed:", e);
|
|
1033
1831
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
for (const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1041
|
-
continue;
|
|
1042
|
-
}
|
|
1043
|
-
const className = keyToClass.get(key);
|
|
1044
|
-
if (className) node.classList.add(className);
|
|
1045
|
-
const bgImage = node.style?.backgroundImage;
|
|
1046
|
-
const hasIcon = node.dataset?.snapdomHasIcon;
|
|
1047
|
-
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
1048
|
-
if (hasIcon) {
|
|
1049
|
-
node.style.verticalAlign = "middle";
|
|
1050
|
-
node.style.display = "inline";
|
|
1051
|
-
}
|
|
1832
|
+
await resolveBlobUrlsInTree(clone);
|
|
1833
|
+
try {
|
|
1834
|
+
const styleNodes = clone.querySelectorAll("style[data-sd]");
|
|
1835
|
+
for (const s of styleNodes) {
|
|
1836
|
+
shadowScopedCSS += s.textContent || "";
|
|
1837
|
+
s.remove();
|
|
1052
1838
|
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
const keyToClass = generateCSSClasses(sessionCache.styleMap);
|
|
1842
|
+
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
1843
|
+
classCSS = shadowScopedCSS + classCSS;
|
|
1844
|
+
for (const [node, key] of sessionCache.styleMap.entries()) {
|
|
1845
|
+
if (node.tagName === "STYLE") continue;
|
|
1846
|
+
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
1056
1847
|
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const className = keyToClass.get(key);
|
|
1851
|
+
if (className) node.classList.add(className);
|
|
1852
|
+
const bgImage = node.style?.backgroundImage;
|
|
1853
|
+
const hasIcon = node.dataset?.snapdomHasIcon;
|
|
1854
|
+
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
1855
|
+
if (hasIcon) {
|
|
1856
|
+
node.style.verticalAlign = "middle";
|
|
1857
|
+
node.style.display = "inline";
|
|
1057
1858
|
}
|
|
1058
1859
|
}
|
|
1059
|
-
for (const [cloneNode, originalNode] of
|
|
1860
|
+
for (const [cloneNode, originalNode] of sessionCache.nodeMap.entries()) {
|
|
1060
1861
|
const scrollX = originalNode.scrollLeft;
|
|
1061
1862
|
const scrollY = originalNode.scrollTop;
|
|
1062
1863
|
const hasScroll = scrollX || scrollY;
|
|
@@ -1075,9 +1876,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1075
1876
|
cloneNode.appendChild(inner);
|
|
1076
1877
|
}
|
|
1077
1878
|
}
|
|
1078
|
-
if (element ===
|
|
1079
|
-
const computed =
|
|
1080
|
-
|
|
1879
|
+
if (element === sessionCache.nodeMap.get(clone)) {
|
|
1880
|
+
const computed = sessionCache.styleCache.get(element) || window.getComputedStyle(element);
|
|
1881
|
+
sessionCache.styleCache.set(element, computed);
|
|
1081
1882
|
const transform = stripTranslate(computed.transform);
|
|
1082
1883
|
clone.style.margin = "0";
|
|
1083
1884
|
clone.style.position = "static";
|
|
@@ -1090,13 +1891,13 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1090
1891
|
clone.style.clear = "none";
|
|
1091
1892
|
clone.style.transform = transform || "";
|
|
1092
1893
|
}
|
|
1093
|
-
for (const [cloneNode, originalNode] of
|
|
1894
|
+
for (const [cloneNode, originalNode] of sessionCache.nodeMap.entries()) {
|
|
1094
1895
|
if (originalNode.tagName === "PRE") {
|
|
1095
1896
|
cloneNode.style.marginTop = "0";
|
|
1096
1897
|
cloneNode.style.marginBlockStart = "0";
|
|
1097
1898
|
}
|
|
1098
1899
|
}
|
|
1099
|
-
return { clone, classCSS };
|
|
1900
|
+
return { clone, classCSS, styleCache: sessionCache.styleCache };
|
|
1100
1901
|
}
|
|
1101
1902
|
function stabilizeLayout(element) {
|
|
1102
1903
|
const style = getComputedStyle(element);
|
|
@@ -1110,23 +1911,168 @@ function stabilizeLayout(element) {
|
|
|
1110
1911
|
element.style.border = `${outlineWidth} solid transparent`;
|
|
1111
1912
|
}
|
|
1112
1913
|
}
|
|
1914
|
+
var _blobToDataUrlCache = /* @__PURE__ */ new Map();
|
|
1915
|
+
async function blobUrlToDataUrl(blobUrl) {
|
|
1916
|
+
if (cache.resource?.has(blobUrl)) return cache.resource.get(blobUrl);
|
|
1917
|
+
if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
|
|
1918
|
+
const p = (async () => {
|
|
1919
|
+
const r = await snapFetch(blobUrl, { as: "dataURL", silent: true });
|
|
1920
|
+
if (!r.ok || typeof r.data !== "string") {
|
|
1921
|
+
throw new Error(`[snapDOM] Failed to read blob URL: ${blobUrl}`);
|
|
1922
|
+
}
|
|
1923
|
+
cache.resource?.set(blobUrl, r.data);
|
|
1924
|
+
return r.data;
|
|
1925
|
+
})();
|
|
1926
|
+
_blobToDataUrlCache.set(blobUrl, p);
|
|
1927
|
+
try {
|
|
1928
|
+
const data = await p;
|
|
1929
|
+
_blobToDataUrlCache.set(blobUrl, data);
|
|
1930
|
+
return data;
|
|
1931
|
+
} catch (e) {
|
|
1932
|
+
_blobToDataUrlCache.delete(blobUrl);
|
|
1933
|
+
throw e;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
|
|
1937
|
+
async function replaceBlobUrlsInCssText(cssText) {
|
|
1938
|
+
if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
|
|
1939
|
+
const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
|
|
1940
|
+
if (uniques.length === 0) return cssText;
|
|
1941
|
+
let out = cssText;
|
|
1942
|
+
for (const u of uniques) {
|
|
1943
|
+
try {
|
|
1944
|
+
const d = await blobUrlToDataUrl(u);
|
|
1945
|
+
out = out.split(u).join(d);
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return out;
|
|
1950
|
+
}
|
|
1951
|
+
function isBlobUrl(u) {
|
|
1952
|
+
return typeof u === "string" && u.startsWith("blob:");
|
|
1953
|
+
}
|
|
1954
|
+
function parseSrcset(srcset) {
|
|
1955
|
+
return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
|
|
1956
|
+
const m = item.match(/^(\S+)(\s+.+)?$/);
|
|
1957
|
+
return m ? { url: m[1], desc: m[2] || "" } : null;
|
|
1958
|
+
}).filter(Boolean);
|
|
1959
|
+
}
|
|
1960
|
+
function stringifySrcset(parts) {
|
|
1961
|
+
return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
|
|
1962
|
+
}
|
|
1963
|
+
async function resolveBlobUrlsInTree(root) {
|
|
1964
|
+
if (!root) return;
|
|
1965
|
+
const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
|
|
1966
|
+
for (const img of imgs) {
|
|
1967
|
+
try {
|
|
1968
|
+
const srcAttr = img.getAttribute("src");
|
|
1969
|
+
const effective = srcAttr || img.currentSrc || "";
|
|
1970
|
+
if (isBlobUrl(effective)) {
|
|
1971
|
+
const data = await blobUrlToDataUrl(effective);
|
|
1972
|
+
img.setAttribute("src", data);
|
|
1973
|
+
}
|
|
1974
|
+
const srcset = img.getAttribute("srcset");
|
|
1975
|
+
if (srcset && srcset.includes("blob:")) {
|
|
1976
|
+
const parts = parseSrcset(srcset);
|
|
1977
|
+
let changed = false;
|
|
1978
|
+
for (const p of parts) {
|
|
1979
|
+
if (isBlobUrl(p.url)) {
|
|
1980
|
+
try {
|
|
1981
|
+
p.url = await blobUrlToDataUrl(p.url);
|
|
1982
|
+
changed = true;
|
|
1983
|
+
} catch {
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (changed) img.setAttribute("srcset", stringifySrcset(parts));
|
|
1988
|
+
}
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
|
|
1993
|
+
for (const node of svgImages) {
|
|
1994
|
+
try {
|
|
1995
|
+
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1996
|
+
const href = node.getAttribute("href") || node.getAttributeNS?.(XLINK_NS, "href");
|
|
1997
|
+
if (isBlobUrl(href)) {
|
|
1998
|
+
const d = await blobUrlToDataUrl(href);
|
|
1999
|
+
node.setAttribute("href", d);
|
|
2000
|
+
node.removeAttributeNS?.(XLINK_NS, "href");
|
|
2001
|
+
}
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
|
|
2006
|
+
for (const el of styled) {
|
|
2007
|
+
try {
|
|
2008
|
+
const styleText = el.getAttribute("style");
|
|
2009
|
+
if (styleText && styleText.includes("blob:")) {
|
|
2010
|
+
const replaced = await replaceBlobUrlsInCssText(styleText);
|
|
2011
|
+
el.setAttribute("style", replaced);
|
|
2012
|
+
}
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
|
|
2017
|
+
for (const s of styleTags) {
|
|
2018
|
+
try {
|
|
2019
|
+
const css = s.textContent || "";
|
|
2020
|
+
if (css.includes("blob:")) {
|
|
2021
|
+
s.textContent = await replaceBlobUrlsInCssText(css);
|
|
2022
|
+
}
|
|
2023
|
+
} catch {
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const urlAttrs = ["poster"];
|
|
2027
|
+
for (const attr of urlAttrs) {
|
|
2028
|
+
const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
|
|
2029
|
+
for (const n of nodes) {
|
|
2030
|
+
try {
|
|
2031
|
+
const u = n.getAttribute(attr);
|
|
2032
|
+
if (isBlobUrl(u)) {
|
|
2033
|
+
n.setAttribute(attr, await blobUrlToDataUrl(u));
|
|
2034
|
+
}
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1113
2040
|
|
|
1114
2041
|
// src/modules/images.js
|
|
1115
2042
|
async function inlineImages(clone, options = {}) {
|
|
1116
2043
|
const imgs = Array.from(clone.querySelectorAll("img"));
|
|
1117
2044
|
const processImg = async (img) => {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
2045
|
+
if (!img.getAttribute("src")) {
|
|
2046
|
+
const eff = img.currentSrc || img.src || "";
|
|
2047
|
+
if (eff) img.setAttribute("src", eff);
|
|
2048
|
+
}
|
|
2049
|
+
img.removeAttribute("srcset");
|
|
2050
|
+
img.removeAttribute("sizes");
|
|
2051
|
+
const src = img.src || "";
|
|
2052
|
+
if (!src) return;
|
|
2053
|
+
const r = await snapFetch(src, { as: "dataURL", useProxy: options.useProxy });
|
|
2054
|
+
if (r.ok && typeof r.data === "string" && r.data.startsWith("data:")) {
|
|
2055
|
+
img.src = r.data;
|
|
1122
2056
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
1123
2057
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
1124
|
-
|
|
1125
|
-
const fallback = document.createElement("div");
|
|
1126
|
-
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;`;
|
|
1127
|
-
fallback.innerText = "img";
|
|
1128
|
-
img.replaceWith(fallback);
|
|
2058
|
+
return;
|
|
1129
2059
|
}
|
|
2060
|
+
const w = img.width || img.naturalWidth || 100;
|
|
2061
|
+
const h = img.height || img.naturalHeight || 100;
|
|
2062
|
+
const fallback = document.createElement("div");
|
|
2063
|
+
fallback.style.cssText = [
|
|
2064
|
+
`width:${w}px`,
|
|
2065
|
+
`height:${h}px`,
|
|
2066
|
+
"background:#ccc",
|
|
2067
|
+
"display:inline-block",
|
|
2068
|
+
"text-align:center",
|
|
2069
|
+
`line-height:${h}px`,
|
|
2070
|
+
"color:#666",
|
|
2071
|
+
"font-size:12px",
|
|
2072
|
+
"overflow:hidden"
|
|
2073
|
+
].join(";");
|
|
2074
|
+
fallback.textContent = "img";
|
|
2075
|
+
img.replaceWith(fallback);
|
|
1130
2076
|
};
|
|
1131
2077
|
for (let i = 0; i < imgs.length; i += 4) {
|
|
1132
2078
|
const group = imgs.slice(i, i + 4).map(processImg);
|
|
@@ -1135,7 +2081,7 @@ async function inlineImages(clone, options = {}) {
|
|
|
1135
2081
|
}
|
|
1136
2082
|
|
|
1137
2083
|
// src/modules/background.js
|
|
1138
|
-
async function inlineBackgroundImages(source, clone, options = {}) {
|
|
2084
|
+
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
1139
2085
|
const queue = [[source, clone]];
|
|
1140
2086
|
const imageProps = [
|
|
1141
2087
|
"background-image",
|
|
@@ -1155,8 +2101,8 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1155
2101
|
];
|
|
1156
2102
|
while (queue.length) {
|
|
1157
2103
|
const [srcNode, cloneNode] = queue.shift();
|
|
1158
|
-
const style =
|
|
1159
|
-
if (!
|
|
2104
|
+
const style = styleCache.get(srcNode) || getStyle(srcNode);
|
|
2105
|
+
if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
|
|
1160
2106
|
const hasBorderImage = (() => {
|
|
1161
2107
|
const bi = style.getPropertyValue("border-image");
|
|
1162
2108
|
const bis = style.getPropertyValue("border-image-source");
|
|
@@ -1176,10 +2122,6 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1176
2122
|
cloneNode.style.setProperty(prop, inlined.join(", "));
|
|
1177
2123
|
}
|
|
1178
2124
|
}
|
|
1179
|
-
const bgColor = style.getPropertyValue("background-color");
|
|
1180
|
-
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
1181
|
-
cloneNode.style.backgroundColor = bgColor;
|
|
1182
|
-
}
|
|
1183
2125
|
const sChildren = Array.from(srcNode.children);
|
|
1184
2126
|
const cChildren = Array.from(cloneNode.children);
|
|
1185
2127
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
@@ -1189,16 +2131,16 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1189
2131
|
}
|
|
1190
2132
|
|
|
1191
2133
|
// src/core/capture.js
|
|
1192
|
-
async function captureDOM(element, options
|
|
2134
|
+
async function captureDOM(element, options) {
|
|
1193
2135
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1194
|
-
cache
|
|
1195
|
-
const
|
|
1196
|
-
let clone, classCSS;
|
|
2136
|
+
applyCachePolicy(options.cache);
|
|
2137
|
+
const fast = options.fast;
|
|
2138
|
+
let clone, classCSS, styleCache;
|
|
1197
2139
|
let fontsCSS = "";
|
|
1198
2140
|
let baseCSS = "";
|
|
1199
2141
|
let dataURL;
|
|
1200
2142
|
let svgString;
|
|
1201
|
-
({ clone, classCSS } = await prepareClone(element,
|
|
2143
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, options));
|
|
1202
2144
|
await new Promise((resolve) => {
|
|
1203
2145
|
idle(async () => {
|
|
1204
2146
|
await inlineImages(clone, options);
|
|
@@ -1207,32 +2149,38 @@ async function captureDOM(element, options = {}) {
|
|
|
1207
2149
|
});
|
|
1208
2150
|
await new Promise((resolve) => {
|
|
1209
2151
|
idle(async () => {
|
|
1210
|
-
await inlineBackgroundImages(element, clone, options);
|
|
2152
|
+
await inlineBackgroundImages(element, clone, styleCache, options);
|
|
1211
2153
|
resolve();
|
|
1212
2154
|
}, { fast });
|
|
1213
2155
|
});
|
|
1214
|
-
if (embedFonts) {
|
|
2156
|
+
if (options.embedFonts) {
|
|
1215
2157
|
await new Promise((resolve) => {
|
|
1216
2158
|
idle(async () => {
|
|
1217
|
-
|
|
2159
|
+
const required = collectUsedFontVariants(element);
|
|
2160
|
+
const usedCodepoints = collectUsedCodepoints(element);
|
|
2161
|
+
if (isSafari) {
|
|
2162
|
+
const families = new Set(
|
|
2163
|
+
Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
|
|
2164
|
+
);
|
|
2165
|
+
await ensureFontsReady(families, 2);
|
|
2166
|
+
}
|
|
2167
|
+
fontsCSS = await embedCustomFonts({ required, usedCodepoints, preCached: false, exclude: options.excludeFonts, useProxy: options.useProxy });
|
|
1218
2168
|
resolve();
|
|
1219
2169
|
}, { fast });
|
|
1220
2170
|
});
|
|
1221
2171
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
2172
|
+
const usedTags = collectUsedTagNames(clone).sort();
|
|
2173
|
+
const tagKey = usedTags.join(",");
|
|
2174
|
+
if (cache.baseStyle.has(tagKey)) {
|
|
2175
|
+
baseCSS = cache.baseStyle.get(tagKey);
|
|
2176
|
+
} else {
|
|
2177
|
+
await new Promise((resolve) => {
|
|
2178
|
+
idle(() => {
|
|
2179
|
+
baseCSS = generateDedupedBaseCSS(usedTags);
|
|
2180
|
+
cache.baseStyle.set(tagKey, baseCSS);
|
|
2181
|
+
resolve();
|
|
2182
|
+
}, { fast });
|
|
2183
|
+
});
|
|
1236
2184
|
}
|
|
1237
2185
|
await new Promise((resolve) => {
|
|
1238
2186
|
idle(() => {
|
|
@@ -1290,23 +2238,73 @@ async function captureDOM(element, options = {}) {
|
|
|
1290
2238
|
return dataURL;
|
|
1291
2239
|
}
|
|
1292
2240
|
|
|
1293
|
-
// src/
|
|
1294
|
-
|
|
2241
|
+
// src/core/context.js
|
|
2242
|
+
function normalizeCachePolicy(v) {
|
|
2243
|
+
if (v === true) return "soft";
|
|
2244
|
+
if (v === false) return "disabled";
|
|
2245
|
+
if (typeof v === "string") {
|
|
2246
|
+
const s = v.toLowerCase().trim();
|
|
2247
|
+
if (s === "disabled" || s === "full" || s === "auto" || s === "soft") return (
|
|
2248
|
+
/** @type {CachePolicy} */
|
|
2249
|
+
s
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
return "soft";
|
|
2253
|
+
}
|
|
2254
|
+
function createContext(options = {}) {
|
|
2255
|
+
const resolvedFormat = options.format ?? "png";
|
|
2256
|
+
const cachePolicy = normalizeCachePolicy(options.cache);
|
|
2257
|
+
return {
|
|
2258
|
+
// Debug & perf
|
|
2259
|
+
debug: options.debug ?? false,
|
|
2260
|
+
fast: options.fast ?? true,
|
|
2261
|
+
scale: options.scale ?? 1,
|
|
2262
|
+
// DOM filters
|
|
2263
|
+
exclude: options.exclude ?? [],
|
|
2264
|
+
filter: options.filter ?? null,
|
|
2265
|
+
// Fuentes
|
|
2266
|
+
embedFonts: options.embedFonts ?? false,
|
|
2267
|
+
iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
|
|
2268
|
+
localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
|
|
2269
|
+
excludeFonts: options.excludeFonts ?? void 0,
|
|
2270
|
+
/** Cache policy (disabled|full|auto|soft). Default: "soft" */
|
|
2271
|
+
/** @type {CachePolicy} */
|
|
2272
|
+
cache: cachePolicy,
|
|
2273
|
+
// Red
|
|
2274
|
+
useProxy: typeof options.useProxy === "string" ? options.useProxy : "",
|
|
2275
|
+
// Salida
|
|
2276
|
+
width: options.width ?? null,
|
|
2277
|
+
height: options.height ?? null,
|
|
2278
|
+
format: resolvedFormat,
|
|
2279
|
+
type: options.type ?? "svg",
|
|
2280
|
+
quality: options.quality ?? 0.92,
|
|
2281
|
+
dpr: options.dpr ?? (window.devicePixelRatio || 1),
|
|
2282
|
+
backgroundColor: options.backgroundColor ?? (["jpg", "jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
|
|
2283
|
+
filename: options.filename ?? "snapDOM"
|
|
2284
|
+
// Plugins (reservado para futura activación)
|
|
2285
|
+
// plugins: normalizePlugins(...),
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/exporters/toImg.js
|
|
2290
|
+
async function toImg(url, options) {
|
|
1295
2291
|
const img = new Image();
|
|
1296
2292
|
img.src = url;
|
|
1297
2293
|
await img.decode();
|
|
1298
|
-
if (scale !== 1) {
|
|
1299
|
-
img.style.width = `${img.naturalWidth * scale}px`;
|
|
1300
|
-
img.style.height = `${img.naturalHeight * scale}px`;
|
|
2294
|
+
if (options.scale !== 1) {
|
|
2295
|
+
img.style.width = `${img.naturalWidth * options.scale}px`;
|
|
2296
|
+
img.style.height = `${img.naturalHeight * options.scale}px`;
|
|
1301
2297
|
}
|
|
1302
2298
|
return img;
|
|
1303
2299
|
}
|
|
1304
|
-
|
|
2300
|
+
|
|
2301
|
+
// src/exporters/toCanvas.js
|
|
2302
|
+
async function toCanvas(url, options) {
|
|
1305
2303
|
const img = new Image();
|
|
1306
|
-
img.src = url;
|
|
1307
2304
|
img.crossOrigin = "anonymous";
|
|
1308
2305
|
img.loading = "eager";
|
|
1309
2306
|
img.decoding = "sync";
|
|
2307
|
+
img.src = url;
|
|
1310
2308
|
const isSafariBrowser = isSafari();
|
|
1311
2309
|
let appended = false;
|
|
1312
2310
|
if (isSafariBrowser) {
|
|
@@ -1314,16 +2312,11 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1314
2312
|
appended = true;
|
|
1315
2313
|
}
|
|
1316
2314
|
await img.decode();
|
|
1317
|
-
if (isSafariBrowser)
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (img.width === 0 || img.height === 0) {
|
|
1321
|
-
if (appended) img.remove();
|
|
1322
|
-
throw new Error("Image failed to load or has no dimensions");
|
|
1323
|
-
}
|
|
1324
|
-
const width = img.naturalWidth * scale;
|
|
1325
|
-
const height = img.naturalHeight * scale;
|
|
2315
|
+
if (isSafariBrowser) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2316
|
+
const width = img.naturalWidth * options.scale;
|
|
2317
|
+
const height = img.naturalHeight * options.scale;
|
|
1326
2318
|
const canvas = document.createElement("canvas");
|
|
2319
|
+
const dpr = options.dpr;
|
|
1327
2320
|
canvas.width = Math.ceil(width * dpr);
|
|
1328
2321
|
canvas.height = Math.ceil(height * dpr);
|
|
1329
2322
|
canvas.style.width = `${width}px`;
|
|
@@ -1334,162 +2327,178 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1334
2327
|
if (appended) img.remove();
|
|
1335
2328
|
return canvas;
|
|
1336
2329
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
quality
|
|
1342
|
-
} = {}) {
|
|
1343
|
-
const mime = {
|
|
1344
|
-
jpg: "image/jpeg",
|
|
1345
|
-
jpeg: "image/jpeg",
|
|
1346
|
-
png: "image/png",
|
|
1347
|
-
webp: "image/webp"
|
|
1348
|
-
}[type] || "image/png";
|
|
2330
|
+
|
|
2331
|
+
// src/exporters/toBlob.js
|
|
2332
|
+
async function toBlob(url, options) {
|
|
2333
|
+
const type = options.type;
|
|
1349
2334
|
if (type === "svg") {
|
|
1350
2335
|
const svgText = decodeURIComponent(url.split(",")[1]);
|
|
1351
2336
|
return new Blob([svgText], { type: "image/svg+xml" });
|
|
1352
2337
|
}
|
|
1353
|
-
const canvas = await
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
temp.width = baseCanvas.width;
|
|
1363
|
-
temp.height = baseCanvas.height;
|
|
1364
|
-
const ctx = temp.getContext("2d");
|
|
1365
|
-
ctx.fillStyle = backgroundColor;
|
|
1366
|
-
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
1367
|
-
ctx.drawImage(baseCanvas, 0, 0);
|
|
1368
|
-
return temp;
|
|
2338
|
+
const canvas = await toCanvas(url, options);
|
|
2339
|
+
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
2340
|
+
return new Promise(
|
|
2341
|
+
(resolve) => finalCanvas.toBlob(
|
|
2342
|
+
(blob) => resolve(blob),
|
|
2343
|
+
`image/${type}`,
|
|
2344
|
+
options.quality
|
|
2345
|
+
)
|
|
2346
|
+
);
|
|
1369
2347
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const canvas = await
|
|
2348
|
+
|
|
2349
|
+
// src/modules/rasterize.js
|
|
2350
|
+
async function rasterize(url, options) {
|
|
2351
|
+
const canvas = await toCanvas(url, options);
|
|
2352
|
+
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
1374
2353
|
const img = new Image();
|
|
1375
|
-
img.src =
|
|
2354
|
+
img.src = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
|
|
1376
2355
|
await img.decode();
|
|
1377
|
-
img.style.width = `${
|
|
1378
|
-
img.style.height = `${
|
|
2356
|
+
img.style.width = `${finalCanvas.width / options.dpr}px`;
|
|
2357
|
+
img.style.height = `${finalCanvas.height / options.dpr}px`;
|
|
1379
2358
|
return img;
|
|
1380
2359
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
2360
|
+
|
|
2361
|
+
// src/exporters/download.js
|
|
2362
|
+
async function download(url, options) {
|
|
2363
|
+
if (options.format === "svg") {
|
|
2364
|
+
const blob = await toBlob(url, { ...options, type: "svg" });
|
|
1384
2365
|
const objectURL = URL.createObjectURL(blob);
|
|
1385
2366
|
const a2 = document.createElement("a");
|
|
1386
2367
|
a2.href = objectURL;
|
|
1387
|
-
a2.download =
|
|
2368
|
+
a2.download = options.filename;
|
|
1388
2369
|
a2.click();
|
|
1389
2370
|
URL.revokeObjectURL(objectURL);
|
|
1390
2371
|
return;
|
|
1391
2372
|
}
|
|
1392
|
-
const
|
|
1393
|
-
const
|
|
1394
|
-
const canvas = await createBackground(url, { dpr, scale }, finalBg);
|
|
1395
|
-
const mime = {
|
|
1396
|
-
jpg: "image/jpeg",
|
|
1397
|
-
jpeg: "image/jpeg",
|
|
1398
|
-
png: "image/png",
|
|
1399
|
-
webp: "image/webp"
|
|
1400
|
-
}[format] || "image/png";
|
|
1401
|
-
const dataURL = canvas.toDataURL(mime);
|
|
2373
|
+
const canvas = await toCanvas(url, options);
|
|
2374
|
+
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
1402
2375
|
const a = document.createElement("a");
|
|
1403
|
-
a.href =
|
|
1404
|
-
a.download =
|
|
2376
|
+
a.href = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
|
|
2377
|
+
a.download = options.filename;
|
|
1405
2378
|
a.click();
|
|
1406
2379
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
2380
|
+
|
|
2381
|
+
// src/api/snapdom.js
|
|
2382
|
+
var INTERNAL_TOKEN = Symbol("snapdom.internal");
|
|
2383
|
+
async function snapdom(element, userOptions) {
|
|
1409
2384
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
return await snapdom.capture(element, options);
|
|
2385
|
+
const context = createContext(userOptions);
|
|
2386
|
+
if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
|
|
2387
|
+
return snapdom.capture(element, context, INTERNAL_TOKEN);
|
|
1414
2388
|
}
|
|
1415
|
-
snapdom.capture = async (el,
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2389
|
+
snapdom.capture = async (el, context, _token) => {
|
|
2390
|
+
if (_token !== INTERNAL_TOKEN) {
|
|
2391
|
+
throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
|
|
2392
|
+
}
|
|
2393
|
+
const url = await captureDOM(el, context);
|
|
2394
|
+
const ensureContext = (opts) => ({ ...context, ...opts || {} });
|
|
2395
|
+
const withFormat = (format) => (opts) => rasterize(url, ensureContext({ ...opts || {}, format }));
|
|
1419
2396
|
return {
|
|
1420
2397
|
url,
|
|
1421
|
-
options,
|
|
1422
2398
|
toRaw: () => url,
|
|
1423
|
-
toImg: (opts
|
|
1424
|
-
toCanvas: (opts
|
|
1425
|
-
toBlob: (opts
|
|
1426
|
-
toPng: (
|
|
1427
|
-
toJpg: (
|
|
1428
|
-
toWebp: (
|
|
1429
|
-
download: (
|
|
2399
|
+
toImg: (opts) => toImg(url, ensureContext(opts)),
|
|
2400
|
+
toCanvas: (opts) => toCanvas(url, ensureContext(opts)),
|
|
2401
|
+
toBlob: (opts) => toBlob(url, ensureContext(opts)),
|
|
2402
|
+
toPng: withFormat("png"),
|
|
2403
|
+
toJpg: withFormat("jpeg"),
|
|
2404
|
+
toWebp: withFormat("webp"),
|
|
2405
|
+
download: (opts) => download(url, ensureContext(opts))
|
|
1430
2406
|
};
|
|
1431
2407
|
};
|
|
1432
|
-
snapdom.toRaw =
|
|
1433
|
-
snapdom.toImg =
|
|
1434
|
-
snapdom.toCanvas =
|
|
1435
|
-
snapdom.toBlob =
|
|
1436
|
-
snapdom.toPng =
|
|
1437
|
-
snapdom.toJpg =
|
|
1438
|
-
snapdom.toWebp =
|
|
1439
|
-
snapdom.download =
|
|
1440
|
-
const {
|
|
1441
|
-
format = "png",
|
|
1442
|
-
filename = "capture",
|
|
1443
|
-
backgroundColor,
|
|
1444
|
-
...rest
|
|
1445
|
-
} = options;
|
|
1446
|
-
const capture = await snapdom.capture(el, rest);
|
|
1447
|
-
return await capture.download({ format, filename, backgroundColor });
|
|
1448
|
-
};
|
|
2408
|
+
snapdom.toRaw = (el, options) => snapdom(el, options).then((result) => result.toRaw());
|
|
2409
|
+
snapdom.toImg = (el, options) => snapdom(el, options).then((result) => result.toImg());
|
|
2410
|
+
snapdom.toCanvas = (el, options) => snapdom(el, options).then((result) => result.toCanvas());
|
|
2411
|
+
snapdom.toBlob = (el, options) => snapdom(el, options).then((result) => result.toBlob());
|
|
2412
|
+
snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then((result) => result.toPng());
|
|
2413
|
+
snapdom.toJpg = (el, options) => snapdom(el, { ...options, format: "jpeg" }).then((result) => result.toJpg());
|
|
2414
|
+
snapdom.toWebp = (el, options) => snapdom(el, { ...options, format: "webp" }).then((result) => result.toWebp());
|
|
2415
|
+
snapdom.download = (el, options) => snapdom(el, options).then((result) => result.download());
|
|
1449
2416
|
|
|
1450
2417
|
// src/api/preCache.js
|
|
1451
2418
|
async function preCache(root = document, options = {}) {
|
|
1452
|
-
const {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2419
|
+
const {
|
|
2420
|
+
embedFonts = true,
|
|
2421
|
+
cacheOpt = "full",
|
|
2422
|
+
useProxy = ""
|
|
2423
|
+
} = options;
|
|
2424
|
+
applyCachePolicy(cacheOpt);
|
|
2425
|
+
try {
|
|
2426
|
+
await document.fonts.ready;
|
|
2427
|
+
} catch {
|
|
1456
2428
|
}
|
|
1457
|
-
await document.fonts.ready;
|
|
1458
2429
|
precacheCommonTags();
|
|
2430
|
+
cache.session = cache.session || {};
|
|
2431
|
+
if (!cache.session.styleCache) {
|
|
2432
|
+
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
await inlineBackgroundImages(
|
|
2436
|
+
root,
|
|
2437
|
+
/* mirror */
|
|
2438
|
+
void 0,
|
|
2439
|
+
cache.session.styleCache,
|
|
2440
|
+
{ useProxy }
|
|
2441
|
+
);
|
|
2442
|
+
} catch {
|
|
2443
|
+
}
|
|
1459
2444
|
let imgEls = [], allEls = [];
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
2445
|
+
try {
|
|
2446
|
+
if (root?.querySelectorAll) {
|
|
2447
|
+
imgEls = Array.from(root.querySelectorAll("img[src]"));
|
|
2448
|
+
allEls = Array.from(root.querySelectorAll("*"));
|
|
2449
|
+
}
|
|
2450
|
+
} catch {
|
|
1463
2451
|
}
|
|
1464
2452
|
const promises = [];
|
|
1465
2453
|
for (const img of imgEls) {
|
|
1466
|
-
const src = img
|
|
2454
|
+
const src = img?.src;
|
|
2455
|
+
if (!src) continue;
|
|
1467
2456
|
if (!cache.image.has(src)) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
);
|
|
2457
|
+
const p = Promise.resolve().then(() => snapFetch(src, { as: "dataURL", useProxy })).then((dataURL) => {
|
|
2458
|
+
cache.image.set(src, dataURL);
|
|
2459
|
+
}).catch(() => {
|
|
2460
|
+
});
|
|
2461
|
+
promises.push(p);
|
|
1472
2462
|
}
|
|
1473
2463
|
}
|
|
1474
2464
|
for (const el of allEls) {
|
|
1475
|
-
|
|
2465
|
+
let bg = "";
|
|
2466
|
+
try {
|
|
2467
|
+
bg = getStyle(el).backgroundImage;
|
|
2468
|
+
} catch {
|
|
2469
|
+
}
|
|
1476
2470
|
if (bg && bg !== "none") {
|
|
1477
|
-
const
|
|
1478
|
-
for (const entry of
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
})
|
|
1484
|
-
);
|
|
2471
|
+
const parts = splitBackgroundImage(bg);
|
|
2472
|
+
for (const entry of parts) {
|
|
2473
|
+
if (entry.startsWith("url(")) {
|
|
2474
|
+
const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
|
|
2475
|
+
});
|
|
2476
|
+
promises.push(p);
|
|
1485
2477
|
}
|
|
1486
2478
|
}
|
|
1487
2479
|
}
|
|
1488
2480
|
}
|
|
1489
2481
|
if (embedFonts) {
|
|
1490
|
-
|
|
2482
|
+
try {
|
|
2483
|
+
const required = collectUsedFontVariants(root);
|
|
2484
|
+
const usedCodepoints = collectUsedCodepoints(root);
|
|
2485
|
+
if (typeof isSafari === "function" ? isSafari() : !!isSafari) {
|
|
2486
|
+
const families = new Set(
|
|
2487
|
+
Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
|
|
2488
|
+
);
|
|
2489
|
+
await ensureFontsReady(families, 2);
|
|
2490
|
+
}
|
|
2491
|
+
await embedCustomFonts({
|
|
2492
|
+
required,
|
|
2493
|
+
usedCodepoints,
|
|
2494
|
+
exclude: options.excludeFonts,
|
|
2495
|
+
localFonts: options.localFonts,
|
|
2496
|
+
useProxy: options.useProxy ?? useProxy
|
|
2497
|
+
});
|
|
2498
|
+
} catch {
|
|
2499
|
+
}
|
|
1491
2500
|
}
|
|
1492
|
-
await Promise.
|
|
2501
|
+
await Promise.allSettled(promises);
|
|
1493
2502
|
}
|
|
1494
2503
|
export {
|
|
1495
2504
|
preCache,
|