@zumer/snapdom 1.9.9 → 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 +123 -84
- package/dist/snapdom.js +1511 -805
- package/dist/snapdom.min.js +5 -3
- package/dist/snapdom.min.mjs +5 -3
- package/dist/snapdom.mjs +1484 -759
- package/package.json +1 -1
- package/types/snapdom.d.ts +171 -60
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,15 +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
|
-
|
|
19
|
+
session: {
|
|
20
|
+
styleMap: /* @__PURE__ */ new Map(),
|
|
21
|
+
styleCache: /* @__PURE__ */ new WeakMap(),
|
|
22
|
+
nodeMap: /* @__PURE__ */ new Map()
|
|
23
|
+
}
|
|
22
24
|
};
|
|
23
|
-
function
|
|
24
|
-
|
|
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
|
+
}
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
// src/utils/
|
|
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
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
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
|
+
]);
|
|
28
320
|
var commonTags = [
|
|
29
321
|
"div",
|
|
30
322
|
"span",
|
|
@@ -51,12 +343,6 @@ var commonTags = [
|
|
|
51
343
|
"h4",
|
|
52
344
|
"h5",
|
|
53
345
|
"h6",
|
|
54
|
-
"svg",
|
|
55
|
-
"path",
|
|
56
|
-
"circle",
|
|
57
|
-
"rect",
|
|
58
|
-
"line",
|
|
59
|
-
"g",
|
|
60
346
|
"table",
|
|
61
347
|
"thead",
|
|
62
348
|
"tbody",
|
|
@@ -66,28 +352,33 @@ var commonTags = [
|
|
|
66
352
|
];
|
|
67
353
|
function precacheCommonTags() {
|
|
68
354
|
for (let tag of commonTags) {
|
|
69
|
-
|
|
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);
|
|
70
359
|
}
|
|
71
360
|
}
|
|
72
361
|
function getDefaultStyleForTag(tagName) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
|
|
77
|
-
if (skipTags.has(tagName)) {
|
|
362
|
+
tagName = String(tagName).toLowerCase();
|
|
363
|
+
if (NO_DEFAULTS_TAGS.has(tagName)) {
|
|
78
364
|
const empty = {};
|
|
79
365
|
cache.defaultStyle.set(tagName, empty);
|
|
80
366
|
return empty;
|
|
81
367
|
}
|
|
368
|
+
if (cache.defaultStyle.has(tagName)) {
|
|
369
|
+
return cache.defaultStyle.get(tagName);
|
|
370
|
+
}
|
|
82
371
|
let sandbox = document.getElementById("snapdom-sandbox");
|
|
83
372
|
if (!sandbox) {
|
|
84
373
|
sandbox = document.createElement("div");
|
|
85
374
|
sandbox.id = "snapdom-sandbox";
|
|
375
|
+
sandbox.setAttribute("data-snapdom-sandbox", "true");
|
|
376
|
+
sandbox.setAttribute("aria-hidden", "true");
|
|
86
377
|
sandbox.style.position = "absolute";
|
|
87
378
|
sandbox.style.left = "-9999px";
|
|
88
379
|
sandbox.style.top = "-9999px";
|
|
89
|
-
sandbox.style.width = "
|
|
90
|
-
sandbox.style.height = "
|
|
380
|
+
sandbox.style.width = "0px";
|
|
381
|
+
sandbox.style.height = "0px";
|
|
91
382
|
sandbox.style.overflow = "hidden";
|
|
92
383
|
document.body.appendChild(sandbox);
|
|
93
384
|
}
|
|
@@ -97,32 +388,55 @@ function getDefaultStyleForTag(tagName) {
|
|
|
97
388
|
const styles = getComputedStyle(el);
|
|
98
389
|
const defaults = {};
|
|
99
390
|
for (let prop of styles) {
|
|
100
|
-
|
|
391
|
+
if (shouldIgnoreProp(prop)) continue;
|
|
392
|
+
const value = styles.getPropertyValue(prop);
|
|
393
|
+
defaults[prop] = value;
|
|
101
394
|
}
|
|
102
395
|
sandbox.removeChild(el);
|
|
103
396
|
cache.defaultStyle.set(tagName, defaults);
|
|
104
397
|
return defaults;
|
|
105
398
|
}
|
|
106
|
-
var
|
|
107
|
-
|
|
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"
|
|
108
418
|
]);
|
|
109
|
-
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
|
+
}
|
|
110
431
|
const entries = [];
|
|
111
|
-
const
|
|
432
|
+
const defaults = getDefaultStyleForTag(tagName);
|
|
112
433
|
for (let [prop, value] of Object.entries(snapshot)) {
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
entries.push(`${prop}:${value}`);
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
const defaultValue = defaultStyles[prop];
|
|
120
|
-
if (value && value !== defaultValue) {
|
|
121
|
-
entries.push(`${prop}:${value}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
434
|
+
if (shouldIgnoreProp(prop)) continue;
|
|
435
|
+
const def = defaults[prop];
|
|
436
|
+
if (value && value !== def) entries.push(`${prop}:${value}`);
|
|
124
437
|
}
|
|
125
|
-
|
|
438
|
+
entries.sort();
|
|
439
|
+
return entries.join(";");
|
|
126
440
|
}
|
|
127
441
|
function collectUsedTagNames(root) {
|
|
128
442
|
const tagSet = /* @__PURE__ */ new Set();
|
|
@@ -143,6 +457,7 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
143
457
|
const styles = cache.defaultStyle.get(tagName);
|
|
144
458
|
if (!styles) continue;
|
|
145
459
|
const key = Object.entries(styles).map(([k, v]) => `${k}:${v};`).sort().join("");
|
|
460
|
+
if (!key) continue;
|
|
146
461
|
if (!groups.has(key)) {
|
|
147
462
|
groups.set(key, []);
|
|
148
463
|
}
|
|
@@ -156,43 +471,12 @@ function generateDedupedBaseCSS(usedTagNames) {
|
|
|
156
471
|
return css;
|
|
157
472
|
}
|
|
158
473
|
function generateCSSClasses(styleMap) {
|
|
159
|
-
const
|
|
474
|
+
const keys = Array.from(new Set(styleMap.values())).filter(Boolean).sort();
|
|
160
475
|
const classMap = /* @__PURE__ */ new Map();
|
|
161
|
-
let
|
|
162
|
-
for (const
|
|
163
|
-
if (!key.trim()) continue;
|
|
164
|
-
classMap.set(key, `c${counter++}`);
|
|
165
|
-
}
|
|
476
|
+
let i = 1;
|
|
477
|
+
for (const k of keys) classMap.set(k, `c${i++}`);
|
|
166
478
|
return classMap;
|
|
167
479
|
}
|
|
168
|
-
|
|
169
|
-
// src/utils/helpers.js
|
|
170
|
-
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
171
|
-
const rawUrl = extractURL(entry);
|
|
172
|
-
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
173
|
-
if (rawUrl) {
|
|
174
|
-
const encodedUrl = safeEncodeURI(rawUrl);
|
|
175
|
-
if (cache.background.has(encodedUrl)) {
|
|
176
|
-
return options.skipInline ? void 0 : `url(${cache.background.get(encodedUrl)})`;
|
|
177
|
-
} else {
|
|
178
|
-
const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
|
|
179
|
-
cache.background.set(encodedUrl, dataUrl);
|
|
180
|
-
return options.skipInline ? void 0 : `url("${dataUrl}")`;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (isGradient || entry === "none") {
|
|
184
|
-
return entry;
|
|
185
|
-
}
|
|
186
|
-
return entry;
|
|
187
|
-
}
|
|
188
|
-
function idle(fn, { fast = false } = {}) {
|
|
189
|
-
if (fast) return fn();
|
|
190
|
-
if ("requestIdleCallback" in window) {
|
|
191
|
-
requestIdleCallback(fn, { timeout: 50 });
|
|
192
|
-
} else {
|
|
193
|
-
setTimeout(fn, 1);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
480
|
function getStyle(el, pseudo = null) {
|
|
197
481
|
if (!(el instanceof Element)) {
|
|
198
482
|
return window.getComputedStyle(el, pseudo);
|
|
@@ -219,192 +503,6 @@ function parseContent(content) {
|
|
|
219
503
|
}
|
|
220
504
|
return clean;
|
|
221
505
|
}
|
|
222
|
-
function extractURL(value) {
|
|
223
|
-
const match = value.match(/url\((['"]?)(.*?)(\1)\)/);
|
|
224
|
-
if (!match) return null;
|
|
225
|
-
const url = match[2].trim();
|
|
226
|
-
if (url.startsWith("#")) return null;
|
|
227
|
-
return url;
|
|
228
|
-
}
|
|
229
|
-
async function fetchResource(url, { useProxy = "" } = {}) {
|
|
230
|
-
async function doFetch(u) {
|
|
231
|
-
const res = await fetch(u);
|
|
232
|
-
if (!res.ok) throw new Error(`[snapdom] Failed to fetch resource: ${u}`);
|
|
233
|
-
return res;
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
return await doFetch(url);
|
|
237
|
-
} catch (e) {
|
|
238
|
-
if (useProxy && typeof useProxy === "string") {
|
|
239
|
-
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
240
|
-
return doFetch(proxied);
|
|
241
|
-
}
|
|
242
|
-
throw e;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
var _inflight = /* @__PURE__ */ new Map();
|
|
246
|
-
var _errorCache = /* @__PURE__ */ new Map();
|
|
247
|
-
function fetchImage(src, { timeout = 3e3, useProxy = "", errorTTL = 8e3 } = {}) {
|
|
248
|
-
function getCrossOriginMode(url) {
|
|
249
|
-
try {
|
|
250
|
-
const parsed = new URL(url, window.location.href);
|
|
251
|
-
return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
|
|
252
|
-
} catch {
|
|
253
|
-
return "anonymous";
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const ok = (data) => ({ ok: true, data });
|
|
257
|
-
const fail = (e) => ({ ok: false, error: e instanceof Error ? e : new Error(String(e)) });
|
|
258
|
-
function fetchBlobAsDataURLSafe(fetchUrl) {
|
|
259
|
-
try {
|
|
260
|
-
return fetch(fetchUrl, {
|
|
261
|
-
mode: "cors",
|
|
262
|
-
credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
|
|
263
|
-
}).then((r) => {
|
|
264
|
-
if (!r.ok) return fail(new Error("HTTP " + r.status));
|
|
265
|
-
return r.blob().then((blob) => new Promise((resolve) => {
|
|
266
|
-
const reader = new FileReader();
|
|
267
|
-
reader.onloadend = () => {
|
|
268
|
-
const base64 = reader.result;
|
|
269
|
-
if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
|
|
270
|
-
resolve(fail(new Error("Invalid image data URL")));
|
|
271
|
-
} else {
|
|
272
|
-
resolve(ok(base64));
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
reader.onerror = () => resolve(fail(new Error("FileReader error")));
|
|
276
|
-
reader.readAsDataURL(blob);
|
|
277
|
-
}));
|
|
278
|
-
}).catch((e) => fail(e));
|
|
279
|
-
} catch (e) {
|
|
280
|
-
return Promise.resolve(fail(e));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function fetchWithFallbackOnceSafe(url) {
|
|
284
|
-
return fetchBlobAsDataURLSafe(url).then((r) => {
|
|
285
|
-
if (r.ok) return r;
|
|
286
|
-
if (useProxy && typeof useProxy === "string") {
|
|
287
|
-
const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
|
|
288
|
-
return fetchBlobAsDataURLSafe(proxied).then((r2) => {
|
|
289
|
-
if (r2.ok) return r2;
|
|
290
|
-
return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
return fail(new Error("[SnapDOM - fetchImage] Fetch failed and no proxy provided"));
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const now = Date.now();
|
|
297
|
-
const until = _errorCache.get(src);
|
|
298
|
-
if (until && until > now) {
|
|
299
|
-
const pr = Promise.reject(new Error("[SnapDOM - fetchImage] Recently failed (cooldown)."));
|
|
300
|
-
pr.catch(() => {
|
|
301
|
-
});
|
|
302
|
-
return pr;
|
|
303
|
-
}
|
|
304
|
-
if (_inflight.has(src)) return _inflight.get(src);
|
|
305
|
-
const crossOriginValue = getCrossOriginMode(src);
|
|
306
|
-
if (cache.image.has(src)) return Promise.resolve(cache.image.get(src));
|
|
307
|
-
if (src.startsWith("data:image/")) {
|
|
308
|
-
cache.image.set(src, src);
|
|
309
|
-
return Promise.resolve(src);
|
|
310
|
-
}
|
|
311
|
-
if (/\.svg(\?.*)?$/i.test(src)) {
|
|
312
|
-
const p2 = (async () => {
|
|
313
|
-
const direct = await (async () => {
|
|
314
|
-
try {
|
|
315
|
-
const res = await fetch(src, {
|
|
316
|
-
mode: "cors",
|
|
317
|
-
credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
|
|
318
|
-
});
|
|
319
|
-
if (!res.ok) return fail(new Error("HTTP " + res.status));
|
|
320
|
-
const svgText = await res.text();
|
|
321
|
-
return ok(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`);
|
|
322
|
-
} catch (e) {
|
|
323
|
-
return fail(e);
|
|
324
|
-
}
|
|
325
|
-
})();
|
|
326
|
-
if (direct.ok) {
|
|
327
|
-
cache.image.set(src, direct.data);
|
|
328
|
-
return direct.data;
|
|
329
|
-
}
|
|
330
|
-
const via = await fetchWithFallbackOnceSafe(src);
|
|
331
|
-
if (via.ok) {
|
|
332
|
-
cache.image.set(src, via.data);
|
|
333
|
-
return via.data;
|
|
334
|
-
}
|
|
335
|
-
_errorCache.set(src, now + errorTTL);
|
|
336
|
-
return Promise.reject(via.error);
|
|
337
|
-
})();
|
|
338
|
-
_inflight.set(src, p2);
|
|
339
|
-
p2.finally(() => _inflight.delete(src));
|
|
340
|
-
p2.catch(() => {
|
|
341
|
-
});
|
|
342
|
-
return p2;
|
|
343
|
-
}
|
|
344
|
-
const p = new Promise((resolve, reject) => {
|
|
345
|
-
let finished = false;
|
|
346
|
-
const img = new Image();
|
|
347
|
-
const finish = (fn) => (arg) => {
|
|
348
|
-
if (finished) return;
|
|
349
|
-
finished = true;
|
|
350
|
-
clearTimeout(timeoutId);
|
|
351
|
-
img.onload = img.onerror = null;
|
|
352
|
-
fn(arg);
|
|
353
|
-
};
|
|
354
|
-
const onSuccess = (d) => {
|
|
355
|
-
cache.image.set(src, d);
|
|
356
|
-
resolve(d);
|
|
357
|
-
};
|
|
358
|
-
const onFinalError = (e) => {
|
|
359
|
-
_errorCache.set(src, Date.now() + errorTTL);
|
|
360
|
-
reject(e);
|
|
361
|
-
};
|
|
362
|
-
const timeoutId = setTimeout(
|
|
363
|
-
finish(() => {
|
|
364
|
-
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
365
|
-
if (r.ok) onSuccess(r.data);
|
|
366
|
-
else onFinalError(new Error("Image load timed out"));
|
|
367
|
-
});
|
|
368
|
-
}),
|
|
369
|
-
timeout
|
|
370
|
-
);
|
|
371
|
-
img.crossOrigin = crossOriginValue;
|
|
372
|
-
img.onload = finish(() => {
|
|
373
|
-
Promise.resolve(img.decode()).then(() => {
|
|
374
|
-
try {
|
|
375
|
-
const canvas = document.createElement("canvas");
|
|
376
|
-
canvas.width = img.naturalWidth || img.width;
|
|
377
|
-
canvas.height = img.naturalHeight || img.height;
|
|
378
|
-
const ctx = canvas.getContext("2d");
|
|
379
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
380
|
-
onSuccess(canvas.toDataURL("image/png"));
|
|
381
|
-
} catch {
|
|
382
|
-
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
383
|
-
if (r.ok) onSuccess(r.data);
|
|
384
|
-
else onFinalError(r.error);
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}).catch(() => {
|
|
388
|
-
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
389
|
-
if (r.ok) onSuccess(r.data);
|
|
390
|
-
else onFinalError(r.error);
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
img.onerror = finish(() => {
|
|
395
|
-
fetchWithFallbackOnceSafe(src).then((r) => {
|
|
396
|
-
if (r.ok) onSuccess(r.data);
|
|
397
|
-
else onFinalError(r.error);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
img.src = src;
|
|
401
|
-
});
|
|
402
|
-
_inflight.set(src, p);
|
|
403
|
-
p.finally(() => _inflight.delete(src));
|
|
404
|
-
p.catch(() => {
|
|
405
|
-
});
|
|
406
|
-
return p;
|
|
407
|
-
}
|
|
408
506
|
function snapshotComputedStyle(style) {
|
|
409
507
|
const snap = {};
|
|
410
508
|
for (let prop of style) {
|
|
@@ -412,36 +510,6 @@ function snapshotComputedStyle(style) {
|
|
|
412
510
|
}
|
|
413
511
|
return snap;
|
|
414
512
|
}
|
|
415
|
-
function isSafari() {
|
|
416
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
417
|
-
}
|
|
418
|
-
function stripTranslate(transform) {
|
|
419
|
-
if (!transform || transform === "none") return "";
|
|
420
|
-
let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
|
|
421
|
-
cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
|
|
422
|
-
const parts = values.split(",").map((s) => s.trim());
|
|
423
|
-
if (parts.length !== 6) return `matrix(${values})`;
|
|
424
|
-
parts[4] = "0";
|
|
425
|
-
parts[5] = "0";
|
|
426
|
-
return `matrix(${parts.join(", ")})`;
|
|
427
|
-
});
|
|
428
|
-
cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
|
|
429
|
-
const parts = values.split(",").map((s) => s.trim());
|
|
430
|
-
if (parts.length !== 16) return `matrix3d(${values})`;
|
|
431
|
-
parts[12] = "0";
|
|
432
|
-
parts[13] = "0";
|
|
433
|
-
return `matrix3d(${parts.join(", ")})`;
|
|
434
|
-
});
|
|
435
|
-
return cleaned.trim().replace(/\s{2,}/g, " ");
|
|
436
|
-
}
|
|
437
|
-
function safeEncodeURI(uri) {
|
|
438
|
-
if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
|
|
439
|
-
try {
|
|
440
|
-
return encodeURI(uri);
|
|
441
|
-
} catch {
|
|
442
|
-
return uri;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
513
|
function splitBackgroundImage(bg) {
|
|
446
514
|
const parts = [];
|
|
447
515
|
let depth = 0;
|
|
@@ -459,48 +527,205 @@ function splitBackgroundImage(bg) {
|
|
|
459
527
|
return parts;
|
|
460
528
|
}
|
|
461
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
|
+
|
|
462
543
|
// src/modules/styles.js
|
|
463
544
|
var snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
464
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
|
+
}
|
|
465
574
|
function snapshotComputedStyleFull(style) {
|
|
466
|
-
const
|
|
467
|
-
const
|
|
575
|
+
const out = {};
|
|
576
|
+
const vis = style.getPropertyValue("visibility");
|
|
468
577
|
for (let i = 0; i < style.length; i++) {
|
|
469
578
|
const prop = style[i];
|
|
470
579
|
let val = style.getPropertyValue(prop);
|
|
471
580
|
if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
|
|
472
581
|
val = "none";
|
|
473
582
|
}
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
if (
|
|
477
|
-
|
|
583
|
+
out[prop] = val;
|
|
584
|
+
}
|
|
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
|
+
};
|
|
621
|
+
}
|
|
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
|
+
};
|
|
635
|
+
}
|
|
636
|
+
async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
637
|
+
if (source.tagName === "STYLE") return;
|
|
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);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/core/clone.js
|
|
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 {
|
|
478
719
|
}
|
|
479
|
-
return
|
|
720
|
+
return css;
|
|
480
721
|
}
|
|
481
|
-
function
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!snapshotCache.has(source)) {
|
|
488
|
-
const snapshot2 = snapshotComputedStyleFull(style);
|
|
489
|
-
snapshotCache.set(source, snapshot2);
|
|
490
|
-
}
|
|
491
|
-
const snapshot = snapshotCache.get(source);
|
|
492
|
-
const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
|
|
493
|
-
if (snapshotKeyCache.has(hash)) {
|
|
494
|
-
styleMap.set(clone, snapshotKeyCache.get(hash));
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const tagName = source.tagName?.toLowerCase() || "div";
|
|
498
|
-
const key = getStyleKey(snapshot, tagName, compress);
|
|
499
|
-
snapshotKeyCache.set(hash, key);
|
|
500
|
-
styleMap.set(clone, key);
|
|
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);
|
|
501
728
|
}
|
|
502
|
-
|
|
503
|
-
// src/core/clone.js
|
|
504
729
|
function freezeImgSrcset(original, cloned) {
|
|
505
730
|
try {
|
|
506
731
|
const chosen = original.currentSrc || original.src || "";
|
|
@@ -513,10 +738,61 @@ function freezeImgSrcset(original, cloned) {
|
|
|
513
738
|
} catch {
|
|
514
739
|
}
|
|
515
740
|
}
|
|
516
|
-
function
|
|
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) {
|
|
517
784
|
if (!node) throw new Error("Invalid node");
|
|
518
785
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
519
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
|
+
}
|
|
520
796
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
521
797
|
return node.cloneNode(true);
|
|
522
798
|
}
|
|
@@ -545,7 +821,7 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
545
821
|
}
|
|
546
822
|
if (typeof options.filter === "function") {
|
|
547
823
|
try {
|
|
548
|
-
if (!options.filter(node
|
|
824
|
+
if (!options.filter(node)) {
|
|
549
825
|
const spacer = document.createElement("div");
|
|
550
826
|
const rect = node.getBoundingClientRect();
|
|
551
827
|
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
|
|
@@ -562,8 +838,8 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
562
838
|
}
|
|
563
839
|
if (node.getAttribute("data-capture") === "placeholder") {
|
|
564
840
|
const clone2 = node.cloneNode(false);
|
|
565
|
-
nodeMap.set(clone2, node);
|
|
566
|
-
inlineAllStyles(node, clone2,
|
|
841
|
+
sessionCache.nodeMap.set(clone2, node);
|
|
842
|
+
inlineAllStyles(node, clone2, sessionCache, options);
|
|
567
843
|
const placeholder = document.createElement("div");
|
|
568
844
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
569
845
|
placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
|
|
@@ -576,14 +852,14 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
576
852
|
img.src = dataURL;
|
|
577
853
|
img.width = node.width;
|
|
578
854
|
img.height = node.height;
|
|
579
|
-
nodeMap.set(img, node);
|
|
580
|
-
inlineAllStyles(node, img,
|
|
855
|
+
sessionCache.nodeMap.set(img, node);
|
|
856
|
+
inlineAllStyles(node, img, sessionCache, options);
|
|
581
857
|
return img;
|
|
582
858
|
}
|
|
583
859
|
let clone;
|
|
584
860
|
try {
|
|
585
861
|
clone = node.cloneNode(false);
|
|
586
|
-
nodeMap.set(clone, node);
|
|
862
|
+
sessionCache.nodeMap.set(clone, node);
|
|
587
863
|
if (node.tagName === "IMG") {
|
|
588
864
|
freezeImgSrcset(node, clone);
|
|
589
865
|
}
|
|
@@ -611,47 +887,56 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {},
|
|
|
611
887
|
if (node instanceof HTMLSelectElement) {
|
|
612
888
|
pendingSelectValue = node.value;
|
|
613
889
|
}
|
|
614
|
-
inlineAllStyles(node, clone,
|
|
890
|
+
inlineAllStyles(node, clone, sessionCache, options);
|
|
615
891
|
if (node.shadowRoot) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
for (const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
} else {
|
|
627
|
-
const shadowFrag = document.createDocumentFragment();
|
|
628
|
-
for (const child of node.shadowRoot.childNodes) {
|
|
629
|
-
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
630
|
-
const cssText = child.textContent || "";
|
|
631
|
-
if (cssText.trim() && compress) {
|
|
632
|
-
styleCache.set(child, cssText);
|
|
633
|
-
}
|
|
634
|
-
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?.() || [];
|
|
635
900
|
}
|
|
636
|
-
|
|
637
|
-
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
901
|
+
for (const an of assigned) clonedAssignedNodes.add(an);
|
|
638
902
|
}
|
|
639
|
-
|
|
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);
|
|
640
921
|
}
|
|
922
|
+
clone.appendChild(shadowFrag);
|
|
641
923
|
}
|
|
642
924
|
if (node.tagName === "SLOT") {
|
|
643
925
|
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
644
926
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
645
927
|
const fragment = document.createDocumentFragment();
|
|
646
928
|
for (const child of nodesToClone) {
|
|
647
|
-
const clonedChild = deepClone(child,
|
|
648
|
-
if (clonedChild)
|
|
929
|
+
const clonedChild = deepClone(child, sessionCache, options);
|
|
930
|
+
if (clonedChild) {
|
|
931
|
+
markSlottedSubtree(clonedChild);
|
|
932
|
+
fragment.appendChild(clonedChild);
|
|
933
|
+
}
|
|
649
934
|
}
|
|
650
935
|
return fragment;
|
|
651
936
|
}
|
|
652
937
|
for (const child of node.childNodes) {
|
|
653
938
|
if (clonedAssignedNodes.has(child)) continue;
|
|
654
|
-
const clonedChild = deepClone(child,
|
|
939
|
+
const clonedChild = deepClone(child, sessionCache, options);
|
|
655
940
|
if (clonedChild) clone.appendChild(clonedChild);
|
|
656
941
|
}
|
|
657
942
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
@@ -694,7 +979,7 @@ function extendIconFonts(fonts) {
|
|
|
694
979
|
}
|
|
695
980
|
}
|
|
696
981
|
}
|
|
697
|
-
function
|
|
982
|
+
function isIconFont2(input) {
|
|
698
983
|
const text = typeof input === "string" ? input : "";
|
|
699
984
|
const candidates = [...defaultIconFonts, ...userIconFonts];
|
|
700
985
|
for (const rx of candidates) {
|
|
@@ -741,247 +1026,620 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
741
1026
|
height
|
|
742
1027
|
};
|
|
743
1028
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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;
|
|
758
1069
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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" };
|
|
1085
|
+
}
|
|
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;
|
|
766
1119
|
}
|
|
767
|
-
return
|
|
1120
|
+
return false;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
768
1123
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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;
|
|
774
1170
|
}
|
|
775
1171
|
}
|
|
776
|
-
|
|
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
|
+
}
|
|
1266
|
+
}
|
|
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
|
+
};
|
|
777
1304
|
}
|
|
778
|
-
const
|
|
779
|
-
|
|
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 = [];
|
|
780
1356
|
for (const styleTag of document.querySelectorAll("style")) {
|
|
781
1357
|
const cssText = styleTag.textContent || "";
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (!
|
|
787
|
-
styleImports.push(importUrl);
|
|
788
|
-
}
|
|
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);
|
|
789
1363
|
}
|
|
790
1364
|
}
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
}
|
|
793
1377
|
let finalCSS = "";
|
|
794
|
-
|
|
1378
|
+
const linkNodes = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((l) => !!l.href);
|
|
1379
|
+
for (const link of linkNodes) {
|
|
795
1380
|
try {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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) {
|
|
1394
|
+
try {
|
|
1395
|
+
const rules = sheet.cssRules || [];
|
|
1396
|
+
cssText = Array.from(rules).map((r) => r.cssText).join("");
|
|
1397
|
+
} catch {
|
|
1398
|
+
}
|
|
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)) {
|
|
814
1411
|
continue;
|
|
815
1412
|
}
|
|
816
|
-
|
|
817
|
-
|
|
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)) {
|
|
818
1420
|
continue;
|
|
819
1421
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
url = new URL(url, link.href).href;
|
|
828
|
-
}
|
|
829
|
-
if (isIconFont(url)) continue;
|
|
830
|
-
if (cache.resource.has(url)) {
|
|
831
|
-
cache.font.add(url);
|
|
832
|
-
inlined = inlined.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (cache.font.has(url)) continue;
|
|
836
|
-
try {
|
|
837
|
-
const fontRes = await fetchResource(url, { useProxy });
|
|
838
|
-
const blob = await fontRes.blob();
|
|
839
|
-
const b64 = await new Promise((resolve) => {
|
|
840
|
-
const reader = new FileReader();
|
|
841
|
-
reader.onload = () => resolve(reader.result);
|
|
842
|
-
reader.readAsDataURL(blob);
|
|
843
|
-
});
|
|
844
|
-
cache.resource.set(url, b64);
|
|
845
|
-
cache.font.add(url);
|
|
846
|
-
inlined = inlined.replace(match[0], `url(${b64})`);
|
|
847
|
-
} catch (e) {
|
|
848
|
-
console.warn("[snapdom] Failed to fetch font resource:", url);
|
|
849
|
-
}
|
|
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;
|
|
850
1429
|
}
|
|
851
|
-
|
|
1430
|
+
const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href) : face;
|
|
1431
|
+
facesOut += newFace + "";
|
|
852
1432
|
}
|
|
853
|
-
finalCSS +=
|
|
1433
|
+
if (facesOut.trim()) finalCSS += facesOut + "";
|
|
854
1434
|
} catch (e) {
|
|
855
|
-
console.warn("[snapdom] Failed to
|
|
1435
|
+
console.warn("[snapdom] Failed to process stylesheet:", link.href);
|
|
856
1436
|
}
|
|
857
1437
|
}
|
|
858
1438
|
for (const sheet of document.styleSheets) {
|
|
859
1439
|
try {
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
let url = rawUrl;
|
|
884
|
-
if (!url.startsWith("http") && !url.startsWith("data:")) {
|
|
885
|
-
url = new URL(url, sheet.href || location.href).href;
|
|
886
|
-
}
|
|
887
|
-
if (isIconFont(url)) continue;
|
|
888
|
-
if (cache.resource.has(url)) {
|
|
889
|
-
cache.font.add(url);
|
|
890
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${cache.resource.get(url)})`);
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
if (cache.font.has(url)) continue;
|
|
894
|
-
try {
|
|
895
|
-
const res = await fetchResource(url, { useProxy });
|
|
896
|
-
const blob = await res.blob();
|
|
897
|
-
const b64 = await new Promise((resolve) => {
|
|
898
|
-
const reader = new FileReader();
|
|
899
|
-
reader.onload = () => resolve(reader.result);
|
|
900
|
-
reader.readAsDataURL(blob);
|
|
901
|
-
});
|
|
902
|
-
cache.resource.set(url, b64);
|
|
903
|
-
cache.font.add(url);
|
|
904
|
-
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
|
|
905
|
-
} catch (e) {
|
|
906
|
-
console.warn("[snapdom] Failed to fetch font URL:", url);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleVal};font-weight:${weightVal};}`;
|
|
910
|
-
}
|
|
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};` : ""}}`;
|
|
911
1463
|
}
|
|
912
1464
|
}
|
|
913
|
-
} catch
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
if (!
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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)) {
|
|
926
1483
|
try {
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
|
|
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 {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
} catch {
|
|
1493
|
+
console.warn("[snapdom] Failed to fetch dynamic font src:", f._snapdomSrc);
|
|
938
1494
|
continue;
|
|
939
1495
|
}
|
|
940
1496
|
}
|
|
941
1497
|
}
|
|
942
|
-
finalCSS += `@font-face{font-family:'${
|
|
1498
|
+
finalCSS += `@font-face{font-family:'${fam}';src:url(${b64});font-style:${f.style || "normal"};font-weight:${f.weight || "normal"};}`;
|
|
943
1499
|
}
|
|
1500
|
+
} catch {
|
|
944
1501
|
}
|
|
945
1502
|
for (const font of localFonts) {
|
|
946
1503
|
if (!font || typeof font !== "object") continue;
|
|
947
|
-
const
|
|
948
|
-
if (!family ||
|
|
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 || "");
|
|
949
1512
|
let b64 = src;
|
|
950
1513
|
if (!b64.startsWith("data:")) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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 {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
} catch {
|
|
1528
|
+
console.warn("[snapdom] Failed to fetch localFonts src:", src);
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
964
1531
|
}
|
|
965
|
-
} else {
|
|
966
|
-
cache.resource.set(src, b64);
|
|
967
|
-
cache.font.add(src);
|
|
968
1532
|
}
|
|
969
|
-
finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};}`;
|
|
1533
|
+
finalCSS += `@font-face{font-family:'${family}';src:url(${b64});font-style:${style};font-weight:${weight};font-stretch:${stretch};}`;
|
|
970
1534
|
}
|
|
971
1535
|
if (finalCSS) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const style = document.createElement("style");
|
|
975
|
-
style.setAttribute("data-snapdom", "embedFonts");
|
|
976
|
-
style.textContent = finalCSS;
|
|
977
|
-
document.head.appendChild(style);
|
|
978
|
-
}
|
|
1536
|
+
finalCSS = dedupeFontFaces(finalCSS);
|
|
1537
|
+
cache.resource?.set(cacheKey, finalCSS);
|
|
979
1538
|
}
|
|
980
1539
|
return finalCSS;
|
|
981
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
|
+
}
|
|
982
1640
|
|
|
983
1641
|
// src/modules/pseudo.js
|
|
984
|
-
async function inlinePseudoElements(source, clone,
|
|
1642
|
+
async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
985
1643
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
986
1644
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
987
1645
|
try {
|
|
@@ -1006,8 +1664,8 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, options
|
|
|
1006
1664
|
span.textContent = first;
|
|
1007
1665
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
1008
1666
|
const snapshot2 = snapshotComputedStyle(style);
|
|
1009
|
-
const key2 = getStyleKey(snapshot2, "span"
|
|
1010
|
-
styleMap.set(span, key2);
|
|
1667
|
+
const key2 = getStyleKey(snapshot2, "span");
|
|
1668
|
+
sessionCache.styleMap.set(span, key2);
|
|
1011
1669
|
const restNode = document.createTextNode(rest);
|
|
1012
1670
|
clone.replaceChild(restNode, textNode);
|
|
1013
1671
|
clone.insertBefore(span, restNode);
|
|
@@ -1027,7 +1685,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, options
|
|
|
1027
1685
|
const borderStyle = style.borderStyle;
|
|
1028
1686
|
const borderWidth = parseFloat(style.borderWidth);
|
|
1029
1687
|
const transform = style.transform;
|
|
1030
|
-
const
|
|
1688
|
+
const isIconFont22 = isIconFont2(fontFamily);
|
|
1031
1689
|
const hasExplicitContent = content !== "none" && cleanContent !== "";
|
|
1032
1690
|
const hasBg = bg && bg !== "none";
|
|
1033
1691
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
@@ -1040,9 +1698,9 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, options
|
|
|
1040
1698
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
1041
1699
|
pseudoEl.style.verticalAlign = "middle";
|
|
1042
1700
|
const snapshot = snapshotComputedStyle(style);
|
|
1043
|
-
const key = getStyleKey(snapshot, "span"
|
|
1044
|
-
styleMap.set(pseudoEl, key);
|
|
1045
|
-
if (
|
|
1701
|
+
const key = getStyleKey(snapshot, "span");
|
|
1702
|
+
sessionCache.styleMap.set(pseudoEl, key);
|
|
1703
|
+
if (isIconFont22 && cleanContent.length === 1) {
|
|
1046
1704
|
const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
1047
1705
|
const imgEl = document.createElement("img");
|
|
1048
1706
|
imgEl.src = dataUrl;
|
|
@@ -1054,15 +1712,15 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, options
|
|
|
1054
1712
|
if (rawUrl?.trim()) {
|
|
1055
1713
|
try {
|
|
1056
1714
|
const imgEl = document.createElement("img");
|
|
1057
|
-
const dataUrl = await
|
|
1058
|
-
imgEl.src = dataUrl;
|
|
1715
|
+
const dataUrl = await snapFetch(safeEncodeURI(rawUrl), { as: "dataURL", useProxy: options.useProxy });
|
|
1716
|
+
imgEl.src = dataUrl.data;
|
|
1059
1717
|
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
1060
1718
|
pseudoEl.appendChild(imgEl);
|
|
1061
1719
|
} catch (e) {
|
|
1062
1720
|
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
1063
1721
|
}
|
|
1064
1722
|
}
|
|
1065
|
-
} else if (!
|
|
1723
|
+
} else if (!isIconFont22 && hasExplicitContent) {
|
|
1066
1724
|
pseudoEl.textContent = cleanContent;
|
|
1067
1725
|
}
|
|
1068
1726
|
if (hasBg) {
|
|
@@ -1090,7 +1748,7 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, options
|
|
|
1090
1748
|
const sChildren = Array.from(source.children);
|
|
1091
1749
|
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
1092
1750
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
1093
|
-
await inlinePseudoElements(sChildren[i], cChildren[i],
|
|
1751
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], sessionCache, options);
|
|
1094
1752
|
}
|
|
1095
1753
|
}
|
|
1096
1754
|
|
|
@@ -1145,12 +1803,15 @@ function inlineExternalDefsAndSymbols(rootElement) {
|
|
|
1145
1803
|
}
|
|
1146
1804
|
|
|
1147
1805
|
// src/core/prepare.js
|
|
1148
|
-
async function prepareClone(element,
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
+
};
|
|
1152
1812
|
let clone;
|
|
1153
1813
|
let classCSS = "";
|
|
1814
|
+
let shadowScopedCSS = "";
|
|
1154
1815
|
stabilizeLayout(element);
|
|
1155
1816
|
try {
|
|
1156
1817
|
inlineExternalDefsAndSymbols(element);
|
|
@@ -1158,43 +1819,45 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1158
1819
|
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1159
1820
|
}
|
|
1160
1821
|
try {
|
|
1161
|
-
clone = deepClone(element,
|
|
1822
|
+
clone = deepClone(element, sessionCache, options, element);
|
|
1162
1823
|
} catch (e) {
|
|
1163
1824
|
console.warn("deepClone failed:", e);
|
|
1164
1825
|
throw e;
|
|
1165
1826
|
}
|
|
1166
1827
|
try {
|
|
1167
|
-
await inlinePseudoElements(element, clone,
|
|
1828
|
+
await inlinePseudoElements(element, clone, sessionCache, options);
|
|
1168
1829
|
} catch (e) {
|
|
1169
1830
|
console.warn("inlinePseudoElements failed:", e);
|
|
1170
1831
|
}
|
|
1171
1832
|
await resolveBlobUrlsInTree(clone);
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
1178
|
-
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1179
|
-
continue;
|
|
1180
|
-
}
|
|
1181
|
-
const className = keyToClass.get(key);
|
|
1182
|
-
if (className) node.classList.add(className);
|
|
1183
|
-
const bgImage = node.style?.backgroundImage;
|
|
1184
|
-
const hasIcon = node.dataset?.snapdomHasIcon;
|
|
1185
|
-
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
1186
|
-
if (hasIcon) {
|
|
1187
|
-
node.style.verticalAlign = "middle";
|
|
1188
|
-
node.style.display = "inline";
|
|
1189
|
-
}
|
|
1833
|
+
try {
|
|
1834
|
+
const styleNodes = clone.querySelectorAll("style[data-sd]");
|
|
1835
|
+
for (const s of styleNodes) {
|
|
1836
|
+
shadowScopedCSS += s.textContent || "";
|
|
1837
|
+
s.remove();
|
|
1190
1838
|
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
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) {
|
|
1194
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";
|
|
1195
1858
|
}
|
|
1196
1859
|
}
|
|
1197
|
-
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1860
|
+
for (const [cloneNode, originalNode] of sessionCache.nodeMap.entries()) {
|
|
1198
1861
|
const scrollX = originalNode.scrollLeft;
|
|
1199
1862
|
const scrollY = originalNode.scrollTop;
|
|
1200
1863
|
const hasScroll = scrollX || scrollY;
|
|
@@ -1213,9 +1876,9 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1213
1876
|
cloneNode.appendChild(inner);
|
|
1214
1877
|
}
|
|
1215
1878
|
}
|
|
1216
|
-
if (element === nodeMap.get(clone)) {
|
|
1217
|
-
const computed = styleCache.get(element) || window.getComputedStyle(element);
|
|
1218
|
-
styleCache.set(element, computed);
|
|
1879
|
+
if (element === sessionCache.nodeMap.get(clone)) {
|
|
1880
|
+
const computed = sessionCache.styleCache.get(element) || window.getComputedStyle(element);
|
|
1881
|
+
sessionCache.styleCache.set(element, computed);
|
|
1219
1882
|
const transform = stripTranslate(computed.transform);
|
|
1220
1883
|
clone.style.margin = "0";
|
|
1221
1884
|
clone.style.position = "static";
|
|
@@ -1228,13 +1891,13 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1228
1891
|
clone.style.clear = "none";
|
|
1229
1892
|
clone.style.transform = transform || "";
|
|
1230
1893
|
}
|
|
1231
|
-
for (const [cloneNode, originalNode] of nodeMap.entries()) {
|
|
1894
|
+
for (const [cloneNode, originalNode] of sessionCache.nodeMap.entries()) {
|
|
1232
1895
|
if (originalNode.tagName === "PRE") {
|
|
1233
1896
|
cloneNode.style.marginTop = "0";
|
|
1234
1897
|
cloneNode.style.marginBlockStart = "0";
|
|
1235
1898
|
}
|
|
1236
1899
|
}
|
|
1237
|
-
return { clone, classCSS, styleCache };
|
|
1900
|
+
return { clone, classCSS, styleCache: sessionCache.styleCache };
|
|
1238
1901
|
}
|
|
1239
1902
|
function stabilizeLayout(element) {
|
|
1240
1903
|
const style = getComputedStyle(element);
|
|
@@ -1250,22 +1913,25 @@ function stabilizeLayout(element) {
|
|
|
1250
1913
|
}
|
|
1251
1914
|
var _blobToDataUrlCache = /* @__PURE__ */ new Map();
|
|
1252
1915
|
async function blobUrlToDataUrl(blobUrl) {
|
|
1916
|
+
if (cache.resource?.has(blobUrl)) return cache.resource.get(blobUrl);
|
|
1253
1917
|
if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
+
}
|
|
1269
1935
|
}
|
|
1270
1936
|
var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
|
|
1271
1937
|
async function replaceBlobUrlsInCssText(cssText) {
|
|
@@ -1382,18 +2048,31 @@ async function inlineImages(clone, options = {}) {
|
|
|
1382
2048
|
}
|
|
1383
2049
|
img.removeAttribute("srcset");
|
|
1384
2050
|
img.removeAttribute("sizes");
|
|
1385
|
-
const src = img.src;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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;
|
|
1389
2056
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
1390
2057
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
1391
|
-
|
|
1392
|
-
const fallback = document.createElement("div");
|
|
1393
|
-
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;`;
|
|
1394
|
-
fallback.innerText = "img";
|
|
1395
|
-
img.replaceWith(fallback);
|
|
2058
|
+
return;
|
|
1396
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);
|
|
1397
2076
|
};
|
|
1398
2077
|
for (let i = 0; i < imgs.length; i += 4) {
|
|
1399
2078
|
const group = imgs.slice(i, i + 4).map(processImg);
|
|
@@ -1443,10 +2122,6 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
1443
2122
|
cloneNode.style.setProperty(prop, inlined.join(", "));
|
|
1444
2123
|
}
|
|
1445
2124
|
}
|
|
1446
|
-
const bgColor = style.getPropertyValue("background-color");
|
|
1447
|
-
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
1448
|
-
cloneNode.style.backgroundColor = bgColor;
|
|
1449
|
-
}
|
|
1450
2125
|
const sChildren = Array.from(srcNode.children);
|
|
1451
2126
|
const cChildren = Array.from(cloneNode.children);
|
|
1452
2127
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
@@ -1456,16 +2131,16 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
1456
2131
|
}
|
|
1457
2132
|
|
|
1458
2133
|
// src/core/capture.js
|
|
1459
|
-
async function captureDOM(element, options
|
|
2134
|
+
async function captureDOM(element, options) {
|
|
1460
2135
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1461
|
-
cache
|
|
1462
|
-
const
|
|
2136
|
+
applyCachePolicy(options.cache);
|
|
2137
|
+
const fast = options.fast;
|
|
1463
2138
|
let clone, classCSS, styleCache;
|
|
1464
2139
|
let fontsCSS = "";
|
|
1465
2140
|
let baseCSS = "";
|
|
1466
2141
|
let dataURL;
|
|
1467
2142
|
let svgString;
|
|
1468
|
-
({ clone, classCSS, styleCache } = await prepareClone(element,
|
|
2143
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, options));
|
|
1469
2144
|
await new Promise((resolve) => {
|
|
1470
2145
|
idle(async () => {
|
|
1471
2146
|
await inlineImages(clone, options);
|
|
@@ -1478,28 +2153,34 @@ async function captureDOM(element, options = {}) {
|
|
|
1478
2153
|
resolve();
|
|
1479
2154
|
}, { fast });
|
|
1480
2155
|
});
|
|
1481
|
-
if (embedFonts) {
|
|
2156
|
+
if (options.embedFonts) {
|
|
1482
2157
|
await new Promise((resolve) => {
|
|
1483
2158
|
idle(async () => {
|
|
1484
|
-
|
|
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 });
|
|
1485
2168
|
resolve();
|
|
1486
2169
|
}, { fast });
|
|
1487
2170
|
});
|
|
1488
2171
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
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
|
+
});
|
|
1503
2184
|
}
|
|
1504
2185
|
await new Promise((resolve) => {
|
|
1505
2186
|
idle(() => {
|
|
@@ -1557,23 +2238,73 @@ async function captureDOM(element, options = {}) {
|
|
|
1557
2238
|
return dataURL;
|
|
1558
2239
|
}
|
|
1559
2240
|
|
|
1560
|
-
// src/
|
|
1561
|
-
|
|
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) {
|
|
1562
2291
|
const img = new Image();
|
|
1563
2292
|
img.src = url;
|
|
1564
2293
|
await img.decode();
|
|
1565
|
-
if (scale !== 1) {
|
|
1566
|
-
img.style.width = `${img.naturalWidth * scale}px`;
|
|
1567
|
-
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`;
|
|
1568
2297
|
}
|
|
1569
2298
|
return img;
|
|
1570
2299
|
}
|
|
1571
|
-
|
|
2300
|
+
|
|
2301
|
+
// src/exporters/toCanvas.js
|
|
2302
|
+
async function toCanvas(url, options) {
|
|
1572
2303
|
const img = new Image();
|
|
1573
|
-
img.src = url;
|
|
1574
2304
|
img.crossOrigin = "anonymous";
|
|
1575
2305
|
img.loading = "eager";
|
|
1576
2306
|
img.decoding = "sync";
|
|
2307
|
+
img.src = url;
|
|
1577
2308
|
const isSafariBrowser = isSafari();
|
|
1578
2309
|
let appended = false;
|
|
1579
2310
|
if (isSafariBrowser) {
|
|
@@ -1581,16 +2312,11 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1581
2312
|
appended = true;
|
|
1582
2313
|
}
|
|
1583
2314
|
await img.decode();
|
|
1584
|
-
if (isSafariBrowser)
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (img.width === 0 || img.height === 0) {
|
|
1588
|
-
if (appended) img.remove();
|
|
1589
|
-
throw new Error("Image failed to load or has no dimensions");
|
|
1590
|
-
}
|
|
1591
|
-
const width = img.naturalWidth * scale;
|
|
1592
|
-
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;
|
|
1593
2318
|
const canvas = document.createElement("canvas");
|
|
2319
|
+
const dpr = options.dpr;
|
|
1594
2320
|
canvas.width = Math.ceil(width * dpr);
|
|
1595
2321
|
canvas.height = Math.ceil(height * dpr);
|
|
1596
2322
|
canvas.style.width = `${width}px`;
|
|
@@ -1601,148 +2327,134 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
|
1601
2327
|
if (appended) img.remove();
|
|
1602
2328
|
return canvas;
|
|
1603
2329
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
quality
|
|
1609
|
-
} = {}) {
|
|
1610
|
-
const mime = {
|
|
1611
|
-
jpg: "image/jpeg",
|
|
1612
|
-
jpeg: "image/jpeg",
|
|
1613
|
-
png: "image/png",
|
|
1614
|
-
webp: "image/webp"
|
|
1615
|
-
}[type] || "image/png";
|
|
2330
|
+
|
|
2331
|
+
// src/exporters/toBlob.js
|
|
2332
|
+
async function toBlob(url, options) {
|
|
2333
|
+
const type = options.type;
|
|
1616
2334
|
if (type === "svg") {
|
|
1617
2335
|
const svgText = decodeURIComponent(url.split(",")[1]);
|
|
1618
2336
|
return new Blob([svgText], { type: "image/svg+xml" });
|
|
1619
2337
|
}
|
|
1620
|
-
const canvas = await
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
temp.width = baseCanvas.width;
|
|
1630
|
-
temp.height = baseCanvas.height;
|
|
1631
|
-
const ctx = temp.getContext("2d");
|
|
1632
|
-
ctx.fillStyle = backgroundColor;
|
|
1633
|
-
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
1634
|
-
ctx.drawImage(baseCanvas, 0, 0);
|
|
1635
|
-
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
|
+
);
|
|
1636
2347
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
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;
|
|
1641
2353
|
const img = new Image();
|
|
1642
|
-
img.src =
|
|
2354
|
+
img.src = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
|
|
1643
2355
|
await img.decode();
|
|
1644
|
-
img.style.width = `${
|
|
1645
|
-
img.style.height = `${
|
|
2356
|
+
img.style.width = `${finalCanvas.width / options.dpr}px`;
|
|
2357
|
+
img.style.height = `${finalCanvas.height / options.dpr}px`;
|
|
1646
2358
|
return img;
|
|
1647
2359
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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" });
|
|
1651
2365
|
const objectURL = URL.createObjectURL(blob);
|
|
1652
2366
|
const a2 = document.createElement("a");
|
|
1653
2367
|
a2.href = objectURL;
|
|
1654
|
-
a2.download =
|
|
2368
|
+
a2.download = options.filename;
|
|
1655
2369
|
a2.click();
|
|
1656
2370
|
URL.revokeObjectURL(objectURL);
|
|
1657
2371
|
return;
|
|
1658
2372
|
}
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
const canvas = await createBackground(url, { dpr, scale }, finalBg);
|
|
1662
|
-
const mime = {
|
|
1663
|
-
jpg: "image/jpeg",
|
|
1664
|
-
jpeg: "image/jpeg",
|
|
1665
|
-
png: "image/png",
|
|
1666
|
-
webp: "image/webp"
|
|
1667
|
-
}[format] || "image/png";
|
|
1668
|
-
const dataURL = canvas.toDataURL(mime);
|
|
2373
|
+
const canvas = await toCanvas(url, options);
|
|
2374
|
+
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
1669
2375
|
const a = document.createElement("a");
|
|
1670
|
-
a.href =
|
|
1671
|
-
a.download =
|
|
2376
|
+
a.href = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
|
|
2377
|
+
a.download = options.filename;
|
|
1672
2378
|
a.click();
|
|
1673
2379
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
2380
|
+
|
|
2381
|
+
// src/api/snapdom.js
|
|
2382
|
+
var INTERNAL_TOKEN = Symbol("snapdom.internal");
|
|
2383
|
+
async function snapdom(element, userOptions) {
|
|
1676
2384
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
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);
|
|
1681
2388
|
}
|
|
1682
|
-
snapdom.capture = async (el,
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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 }));
|
|
1686
2396
|
return {
|
|
1687
2397
|
url,
|
|
1688
|
-
options,
|
|
1689
2398
|
toRaw: () => url,
|
|
1690
|
-
toImg: (opts
|
|
1691
|
-
toCanvas: (opts
|
|
1692
|
-
toBlob: (opts
|
|
1693
|
-
toPng: (
|
|
1694
|
-
toJpg: (
|
|
1695
|
-
toWebp: (
|
|
1696
|
-
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))
|
|
1697
2406
|
};
|
|
1698
2407
|
};
|
|
1699
|
-
snapdom.toRaw =
|
|
1700
|
-
snapdom.toImg =
|
|
1701
|
-
snapdom.toCanvas =
|
|
1702
|
-
snapdom.toBlob =
|
|
1703
|
-
snapdom.toPng =
|
|
1704
|
-
snapdom.toJpg =
|
|
1705
|
-
snapdom.toWebp =
|
|
1706
|
-
snapdom.download =
|
|
1707
|
-
const {
|
|
1708
|
-
format = "png",
|
|
1709
|
-
filename = "capture",
|
|
1710
|
-
backgroundColor,
|
|
1711
|
-
...rest
|
|
1712
|
-
} = options;
|
|
1713
|
-
const capture = await snapdom.capture(el, rest);
|
|
1714
|
-
return await capture.download({ format, filename, backgroundColor });
|
|
1715
|
-
};
|
|
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());
|
|
1716
2416
|
|
|
1717
2417
|
// src/api/preCache.js
|
|
1718
2418
|
async function preCache(root = document, options = {}) {
|
|
1719
|
-
const {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
cache.baseStyle.clear();
|
|
1726
|
-
cache.font.clear();
|
|
1727
|
-
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
2419
|
+
const {
|
|
2420
|
+
embedFonts = true,
|
|
2421
|
+
cacheOpt = "full",
|
|
2422
|
+
useProxy = ""
|
|
2423
|
+
} = options;
|
|
2424
|
+
applyCachePolicy(cacheOpt);
|
|
1730
2425
|
try {
|
|
1731
2426
|
await document.fonts.ready;
|
|
1732
2427
|
} catch {
|
|
1733
2428
|
}
|
|
1734
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
|
+
}
|
|
1735
2444
|
let imgEls = [], allEls = [];
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2445
|
+
try {
|
|
2446
|
+
if (root?.querySelectorAll) {
|
|
2447
|
+
imgEls = Array.from(root.querySelectorAll("img[src]"));
|
|
2448
|
+
allEls = Array.from(root.querySelectorAll("*"));
|
|
2449
|
+
}
|
|
2450
|
+
} catch {
|
|
1739
2451
|
}
|
|
1740
2452
|
const promises = [];
|
|
1741
2453
|
for (const img of imgEls) {
|
|
1742
2454
|
const src = img?.src;
|
|
1743
2455
|
if (!src) continue;
|
|
1744
2456
|
if (!cache.image.has(src)) {
|
|
1745
|
-
const p = Promise.resolve().then(() =>
|
|
2457
|
+
const p = Promise.resolve().then(() => snapFetch(src, { as: "dataURL", useProxy })).then((dataURL) => {
|
|
1746
2458
|
cache.image.set(src, dataURL);
|
|
1747
2459
|
}).catch(() => {
|
|
1748
2460
|
});
|
|
@@ -1756,8 +2468,8 @@ async function preCache(root = document, options = {}) {
|
|
|
1756
2468
|
} catch {
|
|
1757
2469
|
}
|
|
1758
2470
|
if (bg && bg !== "none") {
|
|
1759
|
-
const
|
|
1760
|
-
for (const entry of
|
|
2471
|
+
const parts = splitBackgroundImage(bg);
|
|
2472
|
+
for (const entry of parts) {
|
|
1761
2473
|
if (entry.startsWith("url(")) {
|
|
1762
2474
|
const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
|
|
1763
2475
|
});
|
|
@@ -1768,10 +2480,23 @@ async function preCache(root = document, options = {}) {
|
|
|
1768
2480
|
}
|
|
1769
2481
|
if (embedFonts) {
|
|
1770
2482
|
try {
|
|
1771
|
-
|
|
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
|
+
});
|
|
1772
2498
|
} catch {
|
|
1773
2499
|
}
|
|
1774
|
-
;
|
|
1775
2500
|
}
|
|
1776
2501
|
await Promise.allSettled(promises);
|
|
1777
2502
|
}
|