@zumer/snapdom 0.9.1 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/snapdom.js CHANGED
@@ -1,34 +1,144 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.0.9.1
4
+ * v.0.9.7
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
8
8
  (() => {
9
+ // src/core/cache.js
10
+ var imageCache = /* @__PURE__ */ new Map();
11
+ var bgCache = /* @__PURE__ */ new Map();
12
+ var resourceCache = /* @__PURE__ */ new Map();
13
+ var defaultStylesCache = /* @__PURE__ */ new Map();
14
+ var baseCSSCache = /* @__PURE__ */ new Map();
15
+ var computedStyleCache = /* @__PURE__ */ new WeakMap();
16
+
9
17
  // src/utils/cssTools.js
10
- function getDefaultStyles() {
11
- const defaultEl = document.createElement("div");
12
- document.body.appendChild(defaultEl);
13
- const styles = window.getComputedStyle(defaultEl);
18
+ var commonTags = [
19
+ "div",
20
+ "span",
21
+ "p",
22
+ "a",
23
+ "img",
24
+ "ul",
25
+ "li",
26
+ "button",
27
+ "input",
28
+ "select",
29
+ "textarea",
30
+ "label",
31
+ "section",
32
+ "article",
33
+ "header",
34
+ "footer",
35
+ "nav",
36
+ "main",
37
+ "aside",
38
+ "h1",
39
+ "h2",
40
+ "h3",
41
+ "h4",
42
+ "h5",
43
+ "h6",
44
+ "svg",
45
+ "path",
46
+ "circle",
47
+ "rect",
48
+ "line",
49
+ "g",
50
+ "table",
51
+ "thead",
52
+ "tbody",
53
+ "tr",
54
+ "td",
55
+ "th"
56
+ ];
57
+ function precacheCommonTags() {
58
+ for (let tag of commonTags) {
59
+ getDefaultStyleForTag(tag);
60
+ }
61
+ }
62
+ function getDefaultStyleForTag(tagName) {
63
+ if (defaultStylesCache.has(tagName)) {
64
+ return defaultStylesCache.get(tagName);
65
+ }
66
+ const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
67
+ if (skipTags.has(tagName)) {
68
+ const empty = {};
69
+ defaultStylesCache.set(tagName, empty);
70
+ return empty;
71
+ }
72
+ let sandbox = document.getElementById("snapdom-sandbox");
73
+ if (!sandbox) {
74
+ sandbox = document.createElement("div");
75
+ sandbox.id = "snapdom-sandbox";
76
+ sandbox.style.position = "absolute";
77
+ sandbox.style.left = "-9999px";
78
+ sandbox.style.top = "-9999px";
79
+ sandbox.style.width = "0";
80
+ sandbox.style.height = "0";
81
+ sandbox.style.overflow = "hidden";
82
+ document.body.appendChild(sandbox);
83
+ }
84
+ const el = document.createElement(tagName);
85
+ el.style.all = "initial";
86
+ sandbox.appendChild(el);
87
+ const styles = getComputedStyle(el);
14
88
  const defaults = {};
15
89
  for (let prop of styles) {
16
90
  defaults[prop] = styles.getPropertyValue(prop);
17
91
  }
18
- document.body.removeChild(defaultEl);
92
+ sandbox.removeChild(el);
93
+ defaultStylesCache.set(tagName, defaults);
19
94
  return defaults;
20
95
  }
21
- function getStyleKey(style, defaults) {
96
+ function getStyleKey(snapshot, tagName, compress = false) {
22
97
  const entries = [];
23
- for (let prop of Array.from(style)) {
24
- const value = style.getPropertyValue(prop);
25
- if (value && value !== defaults[prop]) {
26
- entries.push(`${prop}:${value}`);
98
+ const defaultStyles = getDefaultStyleForTag(tagName);
99
+ for (let [prop, value] of Object.entries(snapshot)) {
100
+ if (!compress) {
101
+ if (value) {
102
+ entries.push(`${prop}:${value}`);
103
+ }
104
+ } else {
105
+ const defaultValue = defaultStyles[prop];
106
+ if (value && value !== defaultValue) {
107
+ entries.push(`${prop}:${value}`);
108
+ }
27
109
  }
28
110
  }
29
111
  return entries.sort().join(";");
30
112
  }
31
- function generateReusableCSSClasses(styleMap) {
113
+ function collectUsedTagNames(root) {
114
+ const tagSet = /* @__PURE__ */ new Set();
115
+ if (root.tagName) {
116
+ tagSet.add(root.tagName.toLowerCase());
117
+ }
118
+ root.querySelectorAll("*").forEach((el) => {
119
+ tagSet.add(el.tagName.toLowerCase());
120
+ });
121
+ return Array.from(tagSet);
122
+ }
123
+ function generateDedupedBaseCSS(usedTagNames) {
124
+ const groups = /* @__PURE__ */ new Map();
125
+ for (let tagName of usedTagNames) {
126
+ const styles = defaultStylesCache.get(tagName);
127
+ if (!styles) continue;
128
+ const key = Object.entries(styles).map(([k, v]) => `${k}:${v};`).sort().join("");
129
+ if (!groups.has(key)) {
130
+ groups.set(key, []);
131
+ }
132
+ groups.get(key).push(tagName);
133
+ }
134
+ let css = "";
135
+ for (let [styleBlock, tagList] of groups.entries()) {
136
+ css += `${tagList.join(",")} { ${styleBlock} }
137
+ `;
138
+ }
139
+ return css;
140
+ }
141
+ function generateCSSClasses(styleMap) {
32
142
  const keySet = new Set(styleMap.values());
33
143
  const classMap = /* @__PURE__ */ new Map();
34
144
  let counter = 1;
@@ -38,19 +148,118 @@
38
148
  return classMap;
39
149
  }
40
150
 
151
+ // src/utils/helpers.js
152
+ function idle(fn, { fast = false } = {}) {
153
+ if (fast) return fn();
154
+ if ("requestIdleCallback" in window) {
155
+ requestIdleCallback(fn, { timeout: 50 });
156
+ } else {
157
+ setTimeout(fn, 1);
158
+ }
159
+ }
160
+ function getStyle(el, pseudo = null) {
161
+ if (!(el instanceof Element)) {
162
+ return window.getComputedStyle(el, pseudo);
163
+ }
164
+ let map = computedStyleCache.get(el);
165
+ if (!map) {
166
+ map = /* @__PURE__ */ new Map();
167
+ computedStyleCache.set(el, map);
168
+ }
169
+ if (!map.has(pseudo)) {
170
+ const st = window.getComputedStyle(el, pseudo);
171
+ map.set(pseudo, st);
172
+ }
173
+ return map.get(pseudo);
174
+ }
175
+ function parseContent(content) {
176
+ let clean = content.replace(/^['"]|['"]$/g, "");
177
+ if (clean.startsWith("\\")) {
178
+ try {
179
+ return String.fromCharCode(parseInt(clean.replace("\\", ""), 16));
180
+ } catch {
181
+ return clean;
182
+ }
183
+ }
184
+ return clean;
185
+ }
186
+ function extractUrl(cssValue) {
187
+ const m = cssValue.match(/url\(["']?([^"')]+)["']?\)/);
188
+ return m ? m[1] : null;
189
+ }
190
+ function isIconFont(familyOrUrl) {
191
+ const iconFontPatterns = [
192
+ /font\s*awesome/i,
193
+ /material\s*icons/i,
194
+ /ionicons/i,
195
+ /glyphicons/i,
196
+ /feather/i,
197
+ /bootstrap\s*icons/i,
198
+ /remix\s*icons/i,
199
+ /heroicons/i
200
+ ];
201
+ return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
202
+ }
203
+ function fetchImage(src, timeout = 3e3) {
204
+ if (imageCache.has(src)) {
205
+ return Promise.resolve(imageCache.get(src));
206
+ }
207
+ return new Promise((resolve, reject) => {
208
+ const timeoutId = setTimeout(() => {
209
+ reject(new Error("Image load timed out"));
210
+ }, timeout);
211
+ const image = new Image();
212
+ image.crossOrigin = "anonymous";
213
+ image.onload = async () => {
214
+ clearTimeout(timeoutId);
215
+ try {
216
+ await image.decode();
217
+ const canvas = document.createElement("canvas");
218
+ canvas.width = image.width;
219
+ canvas.height = image.height;
220
+ const ctx = canvas.getContext("2d");
221
+ ctx.drawImage(image, 0, 0);
222
+ try {
223
+ const dataURL = canvas.toDataURL("image/png");
224
+ imageCache.set(src, dataURL);
225
+ resolve(dataURL);
226
+ } catch (e) {
227
+ reject(new Error("CORS restrictions prevented image capture"));
228
+ }
229
+ } catch (e) {
230
+ reject(e);
231
+ }
232
+ };
233
+ image.onerror = (e) => {
234
+ clearTimeout(timeoutId);
235
+ reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
236
+ };
237
+ image.src = src;
238
+ });
239
+ }
240
+ function snapshotComputedStyle(style) {
241
+ const snap = {};
242
+ for (let prop of style) {
243
+ snap[prop] = style.getPropertyValue(prop);
244
+ }
245
+ return snap;
246
+ }
247
+
41
248
  // src/modules/styles.js
42
- function inlineAllStyles(source, clone, styleMap, defaults, cache) {
249
+ function inlineAllStyles(source, clone, styleMap, cache, compress) {
43
250
  if (source.tagName === "STYLE") return;
44
251
  if (!cache.has(source)) {
45
- cache.set(source, window.getComputedStyle(source));
252
+ cache.set(source, getStyle(source));
46
253
  }
47
254
  const style = cache.get(source);
48
- const key = getStyleKey(style, defaults);
255
+ const snapshot = snapshotComputedStyle(style);
256
+ const tagName = source.tagName?.toLowerCase() || "div";
257
+ const key = getStyleKey(snapshot, tagName, compress);
49
258
  styleMap.set(clone, key);
50
259
  }
51
260
 
52
261
  // src/core/clone.js
53
- function deepCloneWithShadow(node, styleMap, defaults, styleCache, nodeMap) {
262
+ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
54
263
  if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
55
264
  const spacer = document.createElement("div");
56
265
  const rect = node.getBoundingClientRect();
@@ -66,7 +275,7 @@
66
275
  if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
67
276
  const clone2 = node.cloneNode(false);
68
277
  nodeMap.set(clone2, node);
69
- inlineAllStyles(node, clone2, styleMap, defaults, styleCache);
278
+ inlineAllStyles(node, clone2, styleMap, styleCache, compress);
70
279
  const placeholder = document.createElement("div");
71
280
  placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
72
281
  placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
@@ -93,187 +302,143 @@
93
302
  if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
94
303
  const clone = node.cloneNode(false);
95
304
  nodeMap.set(clone, node);
96
- inlineAllStyles(node, clone, styleMap, defaults, styleCache);
305
+ inlineAllStyles(node, clone, styleMap, styleCache, compress);
97
306
  const frag = document.createDocumentFragment();
98
307
  node.childNodes.forEach((child) => {
99
- const clonedChild = deepCloneWithShadow(child, styleMap, defaults, styleCache, nodeMap);
308
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
100
309
  if (clonedChild) frag.appendChild(clonedChild);
101
310
  });
102
311
  clone.appendChild(frag);
103
312
  if (node.shadowRoot) {
104
- const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepCloneWithShadow(el, styleMap, defaults, styleCache, nodeMap)).filter(Boolean);
313
+ const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap)).filter(Boolean);
105
314
  const shadowFrag = document.createDocumentFragment();
106
315
  shadowContent.forEach((child) => shadowFrag.appendChild(child));
107
316
  clone.appendChild(shadowFrag);
108
317
  }
109
- clone.querySelectorAll("style").forEach((tag) => tag.removeAttribute("style"));
110
- if (clone.tagName === "STYLE") clone.removeAttribute("style");
111
318
  return clone;
112
319
  }
113
320
 
114
- // src/utils/fetchImage.js
115
- function fetchImageAsDataURL(src, timeout = 3e3) {
116
- return new Promise((resolve, reject) => {
117
- const timeoutId = setTimeout(() => {
118
- reject(new Error("Image load timed out"));
119
- }, timeout);
120
- const image = new Image();
121
- image.crossOrigin = "anonymous";
122
- image.onload = async () => {
123
- clearTimeout(timeoutId);
124
- try {
125
- await image.decode();
126
- const canvas = document.createElement("canvas");
127
- canvas.width = image.width;
128
- canvas.height = image.height;
129
- const ctx = canvas.getContext("2d");
130
- ctx.drawImage(image, 0, 0);
131
- try {
132
- const dataURL = canvas.toDataURL("image/png");
133
- resolve(dataURL);
134
- } catch (e) {
135
- reject(new Error("CORS restrictions prevented image capture"));
136
- }
137
- } catch (e) {
138
- reject(e);
139
- }
140
- };
141
- image.onerror = (e) => {
142
- clearTimeout(timeoutId);
143
- reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
144
- };
145
- image.src = src;
146
- });
147
- }
148
-
149
- // src/utils/delay.js
150
- function delay(ms = 0) {
151
- return new Promise((resolve) => setTimeout(resolve, ms));
321
+ // src/modules/fonts.js
322
+ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
323
+ fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
324
+ await document.fonts.ready;
325
+ await document.fonts.load(`${fontSize}px "${fontFamily}"`);
326
+ const canvas = document.createElement("canvas");
327
+ canvas.width = fontSize;
328
+ canvas.height = fontSize;
329
+ const ctx = canvas.getContext("2d");
330
+ ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : ` ${fontSize}px "${fontFamily}"`;
331
+ ctx.textAlign = "center";
332
+ ctx.textBaseline = "middle";
333
+ ctx.fillStyle = color;
334
+ ctx.fillText(unicodeChar, canvas.width / 2, canvas.height / 2);
335
+ return canvas.toDataURL();
152
336
  }
153
-
154
- // src/modules/images.js
155
- async function inlineImages(clone) {
156
- const imgs = Array.from(clone.querySelectorAll("img"));
157
- const processImg = async (img) => {
158
- const src = img.src;
337
+ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false }) {
338
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
339
+ let finalCSS = "";
340
+ for (const link of links) {
159
341
  try {
160
- const dataUrl = await fetchImageAsDataURL(src);
161
- img.src = dataUrl;
162
- if (!img.width) img.width = img.naturalWidth || 100;
163
- if (!img.height) img.height = img.naturalHeight || 100;
164
- } catch {
165
- const fallback = document.createElement("div");
166
- fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
167
- fallback.innerText = "img";
168
- img.replaceWith(fallback);
169
- }
170
- };
171
- for (let i = 0; i < imgs.length; i += 4) {
172
- const group = imgs.slice(i, i + 4).map(processImg);
173
- await Promise.allSettled(group);
174
- await delay(1);
175
- }
176
- }
177
-
178
- // src/modules/background.js
179
- async function inlineBackgroundImages(source, clone, styleCache) {
180
- const queue = [[source, clone]];
181
- while (queue.length) {
182
- const [srcNode, cloneNode] = queue.shift();
183
- const style = styleCache.get(srcNode) || window.getComputedStyle(srcNode);
184
- if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
185
- const bg = style.getPropertyValue("background-image");
186
- if (bg && bg.includes("url(")) {
187
- const match = bg.match(/url\(["']?([^"')]+)["']?\)/);
188
- if (match?.[1]) {
189
- try {
190
- const dataUrl = await fetchImageAsDataURL(match[1]);
191
- cloneNode.style.backgroundImage = `url(${dataUrl})`;
192
- } catch {
193
- cloneNode.style.backgroundImage = "none";
342
+ const res = await fetch(link.href);
343
+ const cssText = await res.text();
344
+ if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) {
345
+ console.log("\u23ED\uFE0F Ignorando icon font CSS:", link.href);
346
+ continue;
347
+ }
348
+ const urlRegex = /url\(([^)]+)\)/g;
349
+ const inlinedCSS = await Promise.all(
350
+ Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
351
+ let url = match[1].replace(/["']/g, "");
352
+ if (!url.startsWith("http")) {
353
+ url = new URL(url, link.href).href;
354
+ }
355
+ if (ignoreIconFonts && isIconFont(url)) {
356
+ console.log("\u23ED\uFE0F Ignorando icon font URL:", url);
357
+ return null;
358
+ }
359
+ if (resourceCache.has(url)) {
360
+ return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
361
+ }
362
+ try {
363
+ const fontRes = await fetch(url);
364
+ const blob = await fontRes.blob();
365
+ const b64 = await new Promise((resolve) => {
366
+ const reader = new FileReader();
367
+ reader.onload = () => resolve(reader.result);
368
+ reader.readAsDataURL(blob);
369
+ });
370
+ resourceCache.set(url, b64);
371
+ return { original: match[0], inlined: `url(${b64})` };
372
+ } catch (err) {
373
+ console.warn("\u274C No pude fetch font", url);
374
+ return null;
375
+ }
376
+ })
377
+ );
378
+ let cssFinal = cssText;
379
+ for (const r of inlinedCSS) {
380
+ if (r) {
381
+ cssFinal = cssFinal.replace(r.original, r.inlined);
194
382
  }
195
383
  }
384
+ finalCSS += cssFinal + "\n";
385
+ } catch (e) {
386
+ console.warn("\u274C No pude fetch CSS", link.href);
196
387
  }
197
- const sChildren = Array.from(srcNode.children);
198
- const cChildren = Array.from(cloneNode.children);
199
- for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
200
- queue.push([sChildren[i], cChildren[i]]);
201
- }
202
- await delay(1);
203
388
  }
389
+ if (finalCSS && preCached) {
390
+ const style = document.createElement("style");
391
+ style.setAttribute("data-snapdom", "embedFonts");
392
+ style.textContent = finalCSS;
393
+ document.head.appendChild(style);
394
+ }
395
+ return finalCSS;
204
396
  }
205
397
 
206
398
  // src/modules/pseudo.js
207
- async function inlinePseudoElements(source, clone) {
208
- const importantProps = [
209
- "position",
210
- "top",
211
- "right",
212
- "bottom",
213
- "left",
214
- "z-index",
215
- "width",
216
- "height",
217
- "min-width",
218
- "max-width",
219
- "min-height",
220
- "max-height",
221
- "margin",
222
- "padding",
223
- "border",
224
- "border-radius",
225
- "background",
226
- "background-color",
227
- "background-image",
228
- "color",
229
- "font",
230
- "font-family",
231
- "font-size",
232
- "font-weight",
233
- "text-align",
234
- "text-decoration",
235
- "line-height",
236
- "letter-spacing",
237
- "display",
238
- "visibility",
239
- "opacity",
240
- "overflow",
241
- "box-shadow",
242
- "transform",
243
- "transition",
244
- "flex",
245
- "grid"
246
- ];
399
+ async function inlinePseudoElements(source, clone, styleMap, styleCache, compress) {
400
+ if (!(source instanceof Element) || !(clone instanceof Element)) return;
247
401
  for (const pseudo of ["::before", "::after"]) {
248
402
  try {
249
- const style = window.getComputedStyle(source, pseudo);
403
+ const style = getStyle(source, pseudo);
404
+ if (!style) continue;
250
405
  const content = style.getPropertyValue("content");
251
- if (content && content !== "none" && content !== '""' && content !== "''") {
406
+ const bg = style.getPropertyValue("background-image");
407
+ const hasContent = content && content !== "none" && content !== '""' && content !== "''";
408
+ const hasBg = bg && bg.startsWith("url(");
409
+ if (hasContent || hasBg) {
410
+ const fontFamily = style.getPropertyValue("font-family");
411
+ const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
412
+ const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
413
+ const color = style.getPropertyValue("color") || "#000";
252
414
  const pseudoEl = document.createElement("span");
253
- pseudoEl.className = `snapdom-pseudo-${pseudo.replace("::", "")}`;
254
- let cleanContent = content.replace(/^\"|\"$/g, "");
255
- if (cleanContent.startsWith("url(")) {
415
+ pseudoEl.dataset.snapdomPseudo = pseudo;
416
+ const snapshot = snapshotComputedStyle(style);
417
+ const key = getStyleKey(snapshot, "span", compress);
418
+ styleMap.set(pseudoEl, key);
419
+ const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(fontFamily);
420
+ let cleanContent = parseContent(content);
421
+ if (isIconFont2 && cleanContent.length === 1) {
422
+ const imgEl = document.createElement("img");
423
+ imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
424
+ imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
425
+ pseudoEl.appendChild(imgEl);
426
+ } else if (cleanContent.startsWith("url(")) {
256
427
  const match = cleanContent.match(/url\(["']?([^"')]+)["']?\)/);
257
428
  if (match?.[1]) {
258
429
  try {
259
430
  const imgEl = document.createElement("img");
260
- const dataUrl = await fetchImageAsDataURL(match[1]);
431
+ const dataUrl = await fetchImage(match[1]);
261
432
  imgEl.src = dataUrl;
262
433
  imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
263
434
  pseudoEl.appendChild(imgEl);
264
435
  } catch (e) {
436
+ console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
265
437
  }
266
438
  }
267
- } else {
439
+ } else if (cleanContent && cleanContent !== "none") {
268
440
  pseudoEl.textContent = cleanContent;
269
441
  }
270
- for (const prop of importantProps) {
271
- const value = style.getPropertyValue(prop);
272
- if (value) {
273
- pseudoEl.style[prop] = value;
274
- }
275
- }
276
- if (!pseudoEl.style.position) pseudoEl.style.position = "absolute";
277
442
  if (pseudo === "::before") {
278
443
  clone.insertBefore(pseudoEl, clone.firstChild);
279
444
  } else {
@@ -281,146 +446,41 @@
281
446
  }
282
447
  }
283
448
  } catch (e) {
449
+ console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
284
450
  }
285
451
  }
286
452
  const sChildren = Array.from(source.children);
287
453
  const cChildren = Array.from(clone.children).filter(
288
- (child) => !child.classList.contains("snapdom-pseudo-before") && !child.classList.contains("snapdom-pseudo-after")
454
+ (child) => !child.dataset.snapdomPseudo
289
455
  );
290
456
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
291
- await inlinePseudoElements(sChildren[i], cChildren[i]);
292
- if (i % 5 === 0) await delay(1);
457
+ await inlinePseudoElements(sChildren[i], cChildren[i], styleMap, styleCache, compress);
293
458
  }
294
459
  }
295
460
 
296
- // src/utils/normalize.js
297
- function normalizeFontName(str) {
298
- return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
299
- }
300
- function normalizeVariation(value) {
301
- const mappings = {
302
- "normal": "400",
303
- "bold": "700",
304
- "italic": "italic",
305
- "oblique": "italic",
306
- "ultra-condensed": "50",
307
- "extra-condensed": "62",
308
- "condensed": "75",
309
- "semi-condensed": "87",
310
- "expanded": "125",
311
- "semi-expanded": "112",
312
- "extra-expanded": "150",
313
- "ultra-expanded": "200"
314
- };
315
- return mappings[value] || value.replace(/%/g, "");
316
- }
317
-
318
- // src/modules/fonts.js
319
- async function inlineFonts(styleCache = /* @__PURE__ */ new WeakMap()) {
320
- await document.fonts.ready;
321
- const fontFaceRules = [];
322
- Array.from(document.styleSheets).forEach((sheet) => {
323
- try {
324
- Array.from(sheet.cssRules).forEach((rule) => {
325
- if (rule instanceof CSSFontFaceRule) {
326
- fontFaceRules.push(rule.cssText);
327
- }
328
- });
329
- } catch (e) {
330
- console.warn("Could not access stylesheet rules:", e);
331
- }
332
- });
333
- const fontData = /* @__PURE__ */ new Map();
334
- const elements = document.querySelectorAll("*");
335
- elements.forEach((el) => {
336
- const style = styleCache.get(el) || window.getComputedStyle(el);
337
- if (!styleCache.has(el)) styleCache.set(el, style);
338
- const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
339
- if (!family) return;
340
- const weight = normalizeVariation(style.fontWeight || "400");
341
- const styleAttr = normalizeVariation(style.fontStyle || "normal");
342
- const stretch = normalizeVariation(style.fontStretch || "normal");
343
- if (!fontData.has(family)) {
344
- fontData.set(family, /* @__PURE__ */ new Set());
345
- }
346
- fontData.get(family).add(`${weight}_${styleAttr}_${stretch}`);
347
- });
348
- const cssClasses = [...fontFaceRules];
349
- for (const [family, variations] of fontData) {
350
- const safeFamily = normalizeFontName(family);
351
- for (const variation of variations) {
352
- const [weight, styleAttr, stretch] = variation.split("_");
353
- const variationClass = `fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`;
354
- const cssProps = [
355
- `font-family:"${family}",sans-serif`,
356
- `font-weight:${weight}`,
357
- `font-style:${styleAttr}`,
358
- `font-stretch:${stretch}%`
359
- ].filter(Boolean).join(";");
360
- cssClasses.push(`.${variationClass}{${cssProps}}`);
361
- }
362
- }
363
- cssClasses.push("*{font-family:inherit;font-weight:inherit;font-style:inherit;}");
364
- return cssClasses.join("");
365
- }
366
- function applyFontClasses(clone, original, styleCache) {
367
- const style = styleCache.get(original) || window.getComputedStyle(original);
368
- const family = style.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
369
- if (!family) return;
370
- const weight = normalizeVariation(style.fontWeight || "400");
371
- const styleAttr = normalizeVariation(style.fontStyle || "normal");
372
- const stretch = normalizeVariation(style.fontStretch || "normal");
373
- const safeFamily = normalizeFontName(family);
374
- clone.classList.add(`fg-f-${safeFamily}-w${weight}-${styleAttr}-s${stretch}`);
375
- }
376
-
377
461
  // src/core/prepare.js
378
- async function prepareClone(element) {
462
+ async function prepareClone(element, compress = false) {
379
463
  const styleMap = /* @__PURE__ */ new Map();
380
464
  const styleCache = /* @__PURE__ */ new WeakMap();
381
465
  const nodeMap = /* @__PURE__ */ new WeakMap();
382
- let defaults;
383
- try {
384
- defaults = getDefaultStyles();
385
- } catch (e) {
386
- console.warn("getDefaultStyles failed:", e);
387
- defaults = {};
388
- }
389
466
  let clone;
390
467
  try {
391
- clone = deepCloneWithShadow(element, styleMap, defaults, styleCache, nodeMap);
468
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
392
469
  } catch (e) {
393
- console.warn("deepCloneWithShadow failed:", e);
470
+ console.warn("deepClone failed:", e);
394
471
  throw e;
395
472
  }
396
473
  try {
397
- await inlineImages(clone);
398
- } catch (e) {
399
- console.warn("inlineImages failed:", e);
400
- }
401
- try {
402
- await inlineBackgroundImages(element, clone, styleCache);
403
- } catch (e) {
404
- console.warn("inlineBackgroundImages failed:", e);
405
- }
406
- try {
407
- await inlinePseudoElements(element, clone);
474
+ await inlinePseudoElements(element, clone, styleMap, styleCache, compress);
408
475
  } catch (e) {
409
476
  console.warn("inlinePseudoElements failed:", e);
410
477
  }
411
- const keyToClass = generateReusableCSSClasses(styleMap);
478
+ const keyToClass = generateCSSClasses(styleMap);
412
479
  const classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
413
480
  for (const [node, key] of styleMap.entries()) {
414
481
  if (node.tagName === "STYLE") continue;
415
482
  const className = keyToClass.get(key);
416
- if (className) {
417
- node.classList.add(className);
418
- try {
419
- applyFontClasses(node, nodeMap.get(node), styleCache);
420
- } catch (e) {
421
- console.warn("applyFontClasses failed for node:", node, e);
422
- }
423
- }
483
+ if (className) node.classList.add(className);
424
484
  const bgImage = node.style?.backgroundImage;
425
485
  node.removeAttribute("style");
426
486
  if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
@@ -428,97 +488,280 @@
428
488
  return { clone, classCSS, styleCache };
429
489
  }
430
490
 
431
- // src/utils/minifySVG.js
432
- function minifySVG(svgString) {
433
- return svgString.replace(/>\s+</g, "><").replace(/\s{2,}/g, " ").replace(/\n/g, "").trim();
491
+ // src/modules/images.js
492
+ async function inlineImages(clone) {
493
+ const imgs = Array.from(clone.querySelectorAll("img"));
494
+ const processImg = async (img) => {
495
+ const src = img.src;
496
+ try {
497
+ const dataUrl = await fetchImage(src);
498
+ img.src = dataUrl;
499
+ if (!img.width) img.width = img.naturalWidth || 100;
500
+ if (!img.height) img.height = img.naturalHeight || 100;
501
+ } catch {
502
+ const fallback = document.createElement("div");
503
+ fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
504
+ fallback.innerText = "img";
505
+ img.replaceWith(fallback);
506
+ }
507
+ };
508
+ for (let i = 0; i < imgs.length; i += 4) {
509
+ const group = imgs.slice(i, i + 4).map(processImg);
510
+ await Promise.allSettled(group);
511
+ }
512
+ }
513
+
514
+ // src/modules/background.js
515
+ async function inlineBackgroundImages(source, clone, styleCache) {
516
+ const queue = [[source, clone]];
517
+ while (queue.length) {
518
+ const [srcNode, cloneNode] = queue.shift();
519
+ const style = styleCache.get(srcNode) || getStyle(srcNode);
520
+ if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
521
+ const bg = style.getPropertyValue("background-image");
522
+ if (bg && bg.includes("url(")) {
523
+ const match = bg.match(/url\(["']?([^"')]+)["']?\)/);
524
+ if (match?.[1]) {
525
+ try {
526
+ const bgUrl = match[1];
527
+ let dataUrl;
528
+ if (bgCache.has(bgUrl)) {
529
+ dataUrl = bgCache.get(bgUrl);
530
+ } else {
531
+ dataUrl = await fetchImage(bgUrl);
532
+ bgCache.set(bgUrl, dataUrl);
533
+ }
534
+ cloneNode.style.backgroundImage = `url(${dataUrl})`;
535
+ } catch {
536
+ cloneNode.style.backgroundImage = "none";
537
+ }
538
+ }
539
+ }
540
+ const sChildren = Array.from(srcNode.children);
541
+ const cChildren = Array.from(cloneNode.children);
542
+ for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
543
+ queue.push([sChildren[i], cChildren[i]]);
544
+ }
545
+ }
434
546
  }
435
547
 
436
548
  // src/core/capture.js
437
- async function capture(element, scale = 1) {
549
+ async function captureDOM(element, options = {}) {
550
+ if (!element) throw new Error("Element cannot be null or undefined");
551
+ const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
438
552
  let clone, classCSS, styleCache;
439
- try {
440
- ({ clone, classCSS, styleCache } = await prepareClone(element));
441
- } catch (e) {
442
- console.error("prepareClone failed:", e);
443
- throw e;
553
+ let fontsCSS = "";
554
+ let baseCSS = "";
555
+ let dataBlob;
556
+ let svgString;
557
+ ({ clone, classCSS, styleCache } = await prepareClone(element, compress));
558
+ await new Promise((resolve) => {
559
+ idle(async () => {
560
+ await inlineImages(clone);
561
+ resolve();
562
+ }, { fast });
563
+ });
564
+ await new Promise((resolve) => {
565
+ idle(async () => {
566
+ await inlineBackgroundImages(element, clone, styleCache);
567
+ resolve();
568
+ }, { fast });
569
+ });
570
+ if (embedFonts) {
571
+ await new Promise((resolve) => {
572
+ idle(async () => {
573
+ fontsCSS = await embedCustomFonts({ ignoreIconFonts: true });
574
+ resolve();
575
+ }, { fast });
576
+ });
444
577
  }
445
- const w = element.offsetWidth * scale;
446
- const h = element.offsetHeight * scale;
447
- let fonts = "";
448
- try {
449
- fonts = await inlineFonts(styleCache);
450
- } catch (e) {
451
- console.warn("inlineFonts failed:", e);
452
- }
453
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
454
- svg.setAttribute("width", w);
455
- svg.setAttribute("height", h);
456
- svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
457
- const fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
458
- fo.setAttribute("width", "100%");
459
- fo.setAttribute("height", "100%");
460
- const div = document.createElement("div");
461
- div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
462
- div.setAttribute("style", `width:${w}px;height:${h}px;`);
463
- const styleTag = document.createElement("style");
464
- styleTag.textContent = fonts + "svg{overflow:visible}" + classCSS;
465
- div.appendChild(styleTag);
466
- div.appendChild(clone);
467
- fo.appendChild(div);
468
- svg.appendChild(fo);
469
- try {
470
- const svgStr = new XMLSerializer().serializeToString(svg);
471
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(minifySVG(svgStr))}`;
472
- } catch (e) {
473
- console.error("SVG serialization failed:", e);
474
- throw e;
578
+ if (compress) {
579
+ const usedTags = collectUsedTagNames(clone).sort();
580
+ const tagKey = usedTags.join(",");
581
+ if (baseCSSCache.has(tagKey)) {
582
+ baseCSS = baseCSSCache.get(tagKey);
583
+ } else {
584
+ await new Promise((resolve) => {
585
+ idle(() => {
586
+ baseCSS = generateDedupedBaseCSS(usedTags);
587
+ baseCSSCache.set(tagKey, baseCSS);
588
+ resolve();
589
+ }, { fast });
590
+ });
591
+ }
592
+ }
593
+ await new Promise((resolve) => {
594
+ idle(() => {
595
+ const rect = element.getBoundingClientRect();
596
+ const w = rect.width * scale;
597
+ const h = rect.height * scale;
598
+ if (scale !== 1) {
599
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
600
+ clone.style.transform = `scale(${scale})`;
601
+ clone.style.transformOrigin = "top left";
602
+ clone.style.width = `${rect.width}px`;
603
+ clone.style.height = `${rect.height}px`;
604
+ }
605
+ const svgNS = "http://www.w3.org/2000/svg";
606
+ const fo = document.createElementNS(svgNS, "foreignObject");
607
+ fo.setAttribute("width", "100%");
608
+ fo.setAttribute("height", "100%");
609
+ const styleTag = document.createElement("style");
610
+ styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
611
+ fo.appendChild(styleTag);
612
+ fo.appendChild(clone);
613
+ const serializer = new XMLSerializer();
614
+ const foString = serializer.serializeToString(fo);
615
+ const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
616
+ const svgFooter = "</svg>";
617
+ svgString = svgHeader + foString + svgFooter;
618
+ const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
619
+ dataBlob = URL.createObjectURL(blob);
620
+ resolve();
621
+ }, { fast });
622
+ });
623
+ const sandbox = document.getElementById("snapdom-sandbox");
624
+ if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
625
+ if (options.dataURL) {
626
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
627
+ } else {
628
+ return dataBlob;
475
629
  }
476
630
  }
477
631
 
478
632
  // src/api/snapdom.js
479
- async function snapdom(el, scale = 1) {
480
- return await capture(el, scale);
633
+ async function snapdom(element, options = {}) {
634
+ if (!element) {
635
+ throw new Error("Element cannot be null or undefined");
636
+ }
637
+ return await captureDOM(element, options);
481
638
  }
482
- snapdom.toImg = async function(el, scale = 1) {
483
- const dataURL = await snapdom(el, scale);
639
+ snapdom.toImg = async (el, options = {}) => {
640
+ const url = await snapdom(el, options);
484
641
  const img = new Image();
485
- img.src = dataURL;
642
+ img.src = url;
486
643
  await img.decode();
487
644
  return img;
488
645
  };
489
- snapdom.toCanvas = async function(el, scale = 1) {
490
- const img = await snapdom.toImg(el, scale);
646
+ snapdom.toCanvas = async (el, options = {}) => {
647
+ const img = await snapdom.toImg(el, options);
491
648
  const canvas = document.createElement("canvas");
492
649
  canvas.width = img.width;
493
650
  canvas.height = img.height;
494
- const ctx = canvas.getContext("2d");
495
- ctx.drawImage(img, 0, 0);
651
+ canvas.getContext("2d").drawImage(img, 0, 0);
496
652
  return canvas;
497
653
  };
498
- snapdom.toPng = async function(el, scale = 1) {
499
- const canvas = await snapdom.toCanvas(el, scale);
654
+ snapdom.toPng = async (el, options = {}) => {
655
+ const canvas = await snapdom.toCanvas(el, options);
500
656
  const img = new Image();
501
657
  img.src = canvas.toDataURL("image/png");
502
658
  await img.decode();
503
659
  return img;
504
660
  };
505
- snapdom.toJpg = async function(el, scale = 1, quality = 0.92) {
506
- const canvas = await snapdom.toCanvas(el, scale);
661
+ snapdom.toJpg = async (el, options = {}) => {
662
+ const { backgroundColor = "#fff", quality = 1 } = options;
663
+ const canvas = await snapdom.toCanvas(el, options);
664
+ const temp = document.createElement("canvas");
665
+ temp.width = canvas.width;
666
+ temp.height = canvas.height;
667
+ const ctx = temp.getContext("2d");
668
+ ctx.fillStyle = backgroundColor;
669
+ ctx.fillRect(0, 0, temp.width, temp.height);
670
+ ctx.drawImage(canvas, 0, 0);
507
671
  const img = new Image();
508
- img.src = canvas.toDataURL("image/jpeg", quality);
672
+ img.src = temp.toDataURL("image/jpeg", quality);
509
673
  await img.decode();
510
674
  return img;
511
675
  };
512
- snapdom.toWebp = async function(el, scale = 1, quality = 0.92) {
513
- const canvas = await snapdom.toCanvas(el, scale);
676
+ snapdom.toWebp = async (el, options = {}) => {
677
+ const { backgroundColor = "#fff", quality = 1 } = options;
678
+ const canvas = await snapdom.toCanvas(el, options);
679
+ const temp = document.createElement("canvas");
680
+ temp.width = canvas.width;
681
+ temp.height = canvas.height;
682
+ const ctx = temp.getContext("2d");
683
+ ctx.fillStyle = backgroundColor;
684
+ ctx.fillRect(0, 0, temp.width, temp.height);
685
+ ctx.drawImage(canvas, 0, 0);
514
686
  const img = new Image();
515
- img.src = canvas.toDataURL("image/webp", quality);
687
+ img.src = temp.toDataURL("image/webp", quality);
516
688
  await img.decode();
517
689
  return img;
518
690
  };
519
- snapdom.toBlob = async function(el, scale = 1) {
520
- const dataURL = await snapdom(el, scale);
521
- const svgXml = decodeURIComponent(dataURL.split(",")[1]);
522
- return new Blob([svgXml], { type: "image/svg+xml" });
691
+ snapdom.toBlob = async (el, options = {}) => {
692
+ const dataUrl = await snapdom(el, options);
693
+ const svgText = decodeURIComponent(dataUrl.split(",")[1]);
694
+ return new Blob([svgText], { type: "image/svg+xml" });
523
695
  };
696
+
697
+ // src/api/preCache.js
698
+ async function preCache(root = document, options = {}) {
699
+ const { embedFonts = true, reset = false, preWarm = true } = options;
700
+ if (reset) {
701
+ imageCache.clear();
702
+ bgCache.clear();
703
+ resourceCache.clear();
704
+ baseCSSCache.clear();
705
+ computedStyleCache.clear();
706
+ return;
707
+ }
708
+ await document.fonts.ready;
709
+ precacheCommonTags();
710
+ let imgEls = [], allEls = [];
711
+ if (root?.querySelectorAll) {
712
+ imgEls = Array.from(root.querySelectorAll("img[src]"));
713
+ allEls = Array.from(root.querySelectorAll("*"));
714
+ }
715
+ const promises = [];
716
+ for (const img of imgEls) {
717
+ const src = img.src;
718
+ if (!imageCache.has(src)) {
719
+ promises.push(
720
+ fetchImage(src).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
721
+ })
722
+ );
723
+ }
724
+ }
725
+ for (const el of allEls) {
726
+ const bg = getComputedStyle(el).backgroundImage;
727
+ const url = extractUrl(bg);
728
+ if (url && !bgCache.has(url)) {
729
+ promises.push(
730
+ fetchImage(url).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
731
+ })
732
+ );
733
+ }
734
+ }
735
+ if (embedFonts) {
736
+ await embedCustomFonts({ ignoreIconFonts: true, preCached: true });
737
+ }
738
+ await Promise.all(promises);
739
+ if (preWarm) {
740
+ const usedTags = collectUsedTagNames(root).sort();
741
+ const tagKey = usedTags.join(",");
742
+ if (!baseCSSCache.has(tagKey)) {
743
+ const css = generateDedupedBaseCSS(usedTags);
744
+ baseCSSCache.set(tagKey, css);
745
+ }
746
+ const clone = root.cloneNode(false);
747
+ await inlinePseudoElements(
748
+ root,
749
+ clone,
750
+ /* @__PURE__ */ new Map(),
751
+ /* @__PURE__ */ new WeakMap(),
752
+ /* compress */
753
+ false
754
+ );
755
+ }
756
+ if (preWarm) {
757
+ try {
758
+ await prepareClone(
759
+ root,
760
+ /* compress: */
761
+ options.preWarm
762
+ );
763
+ } catch (e) {
764
+ }
765
+ }
766
+ }
524
767
  })();