@zumer/snapdom 0.9.2 → 0.9.8

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