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