@zumer/snapdom 2.0.0-dev.1 → 2.0.0-dev.3

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,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.2.0.0-dev.1
4
+ * v.2.0.0-dev.3
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -328,19 +328,6 @@ async function snapFetch(url, options = {}) {
328
328
  }
329
329
 
330
330
  // src/utils/image.js
331
- function createBackground(baseCanvas, backgroundColor) {
332
- if (!backgroundColor || !baseCanvas.width || !baseCanvas.height) {
333
- return baseCanvas;
334
- }
335
- const temp = document.createElement("canvas");
336
- temp.width = baseCanvas.width;
337
- temp.height = baseCanvas.height;
338
- const ctx = temp.getContext("2d");
339
- ctx.fillStyle = backgroundColor;
340
- ctx.fillRect(0, 0, temp.width, temp.height);
341
- ctx.drawImage(baseCanvas, 0, 0);
342
- return temp;
343
- }
344
331
  async function inlineSingleBackgroundEntry(entry, options = {}) {
345
332
  const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
346
333
  if (isGradient || entry.trim() === "none") {
@@ -616,11 +603,20 @@ function idle(fn, { fast = false } = {}) {
616
603
  }
617
604
  }
618
605
  function isSafari() {
619
- const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
620
- const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
621
- const isUIWebView = /AppleWebKit/i.test(ua) && /Mobile/i.test(ua) && !/Safari/i.test(ua);
622
- const isWeChatUA = /(MicroMessenger|wxwork|WeCom|WindowsWechat|MacWechat)/i.test(ua);
623
- return isSafariUA || isUIWebView || isWeChatUA;
606
+ if (typeof navigator === "undefined") return false;
607
+ const ua = navigator.userAgent || "";
608
+ const uaLower = ua.toLowerCase();
609
+ const isSafariUA = uaLower.includes("safari") && !uaLower.includes("chrome") && !uaLower.includes("crios") && // Chrome on iOS
610
+ !uaLower.includes("fxios") && // Firefox on iOS
611
+ !uaLower.includes("android");
612
+ const isWebKit = /applewebkit/i.test(ua);
613
+ const isMobile = /mobile/i.test(ua);
614
+ const missingSafariToken = !/safari/i.test(ua);
615
+ const isUIWebView = isWebKit && isMobile && missingSafariToken;
616
+ const isWeChatUA = /(micromessenger|wxwork|wecom|windowswechat|macwechat)/i.test(ua);
617
+ const isBaiduUA = /(baiduboxapp|baidubrowser|baidusearch|baiduboxlite)/i.test(uaLower);
618
+ const isIOSWebKit = /ipad|iphone|ipod/.test(uaLower) && isWebKit;
619
+ return isSafariUA || isUIWebView || isWeChatUA || isBaiduUA || isIOSWebKit;
624
620
  }
625
621
 
626
622
  // src/modules/styles.js
@@ -665,6 +661,22 @@ function snapshotComputedStyleFull(style, options = {}) {
665
661
  }
666
662
  out[prop] = val;
667
663
  }
664
+ const EXTRA_TEXT_DECORATION_PROPS = [
665
+ "text-decoration-line",
666
+ "text-decoration-color",
667
+ "text-decoration-style",
668
+ "text-decoration-thickness",
669
+ "text-underline-offset",
670
+ "text-decoration-skip-ink"
671
+ ];
672
+ for (const prop of EXTRA_TEXT_DECORATION_PROPS) {
673
+ if (out[prop]) continue;
674
+ try {
675
+ const v = style.getPropertyValue(prop);
676
+ if (v) out[prop] = v;
677
+ } catch {
678
+ }
679
+ }
668
680
  if (options.embedFonts) {
669
681
  const EXTRA_FONT_PROPS = [
670
682
  "font-feature-settings",
@@ -907,7 +919,7 @@ function resolveCSSVars(sourceEl, cloneEl) {
907
919
  }
908
920
  }
909
921
 
910
- // src/core/clone.js
922
+ // src/utils/clone.helpers.js
911
923
  function idleCallback(childList, callback, fast) {
912
924
  return Promise.all(childList.map((child) => {
913
925
  return new Promise((resolve) => {
@@ -1119,6 +1131,134 @@ async function rasterizeIframe(iframe, sessionCache, options) {
1119
1131
  wrapper.appendChild(imgEl);
1120
1132
  return wrapper;
1121
1133
  }
1134
+ var _blobToDataUrlCache = /* @__PURE__ */ new Map();
1135
+ async function blobUrlToDataUrl(blobUrl) {
1136
+ if (cache.resource?.has(blobUrl)) return cache.resource.get(blobUrl);
1137
+ if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
1138
+ const p = (async () => {
1139
+ const r = await snapFetch(blobUrl, { as: "dataURL", silent: true });
1140
+ if (!r.ok || typeof r.data !== "string") {
1141
+ throw new Error(`[snapDOM] Failed to read blob URL: ${blobUrl}`);
1142
+ }
1143
+ cache.resource?.set(blobUrl, r.data);
1144
+ return r.data;
1145
+ })();
1146
+ _blobToDataUrlCache.set(blobUrl, p);
1147
+ try {
1148
+ const data = await p;
1149
+ _blobToDataUrlCache.set(blobUrl, data);
1150
+ return data;
1151
+ } catch (e) {
1152
+ _blobToDataUrlCache.delete(blobUrl);
1153
+ throw e;
1154
+ }
1155
+ }
1156
+ var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
1157
+ async function replaceBlobUrlsInCssText(cssText) {
1158
+ if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
1159
+ const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
1160
+ if (uniques.length === 0) return cssText;
1161
+ let out = cssText;
1162
+ for (const u of uniques) {
1163
+ try {
1164
+ const d = await blobUrlToDataUrl(u);
1165
+ out = out.split(u).join(d);
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ return out;
1170
+ }
1171
+ function isBlobUrl(u) {
1172
+ return typeof u === "string" && u.startsWith("blob:");
1173
+ }
1174
+ function parseSrcset(srcset) {
1175
+ return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
1176
+ const m = item.match(/^(\S+)(\s+.+)?$/);
1177
+ return m ? { url: m[1], desc: m[2] || "" } : null;
1178
+ }).filter(Boolean);
1179
+ }
1180
+ function stringifySrcset(parts) {
1181
+ return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
1182
+ }
1183
+ async function resolveBlobUrlsInTree(root) {
1184
+ if (!root) return;
1185
+ const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
1186
+ for (const img of imgs) {
1187
+ try {
1188
+ const srcAttr = img.getAttribute("src");
1189
+ const effective = srcAttr || img.currentSrc || "";
1190
+ if (isBlobUrl(effective)) {
1191
+ const data = await blobUrlToDataUrl(effective);
1192
+ img.setAttribute("src", data);
1193
+ }
1194
+ const srcset = img.getAttribute("srcset");
1195
+ if (srcset && srcset.includes("blob:")) {
1196
+ const parts = parseSrcset(srcset);
1197
+ let changed = false;
1198
+ for (const p of parts) {
1199
+ if (isBlobUrl(p.url)) {
1200
+ try {
1201
+ p.url = await blobUrlToDataUrl(p.url);
1202
+ changed = true;
1203
+ } catch {
1204
+ }
1205
+ }
1206
+ }
1207
+ if (changed) img.setAttribute("srcset", stringifySrcset(parts));
1208
+ }
1209
+ } catch {
1210
+ }
1211
+ }
1212
+ const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
1213
+ for (const node of svgImages) {
1214
+ try {
1215
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
1216
+ const href = node.getAttribute("href") || node.getAttributeNS?.(XLINK_NS, "href");
1217
+ if (isBlobUrl(href)) {
1218
+ const d = await blobUrlToDataUrl(href);
1219
+ node.setAttribute("href", d);
1220
+ node.removeAttributeNS?.(XLINK_NS, "href");
1221
+ }
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
1226
+ for (const el of styled) {
1227
+ try {
1228
+ const styleText = el.getAttribute("style");
1229
+ if (styleText && styleText.includes("blob:")) {
1230
+ const replaced = await replaceBlobUrlsInCssText(styleText);
1231
+ el.setAttribute("style", replaced);
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
1237
+ for (const s of styleTags) {
1238
+ try {
1239
+ const css = s.textContent || "";
1240
+ if (css.includes("blob:")) {
1241
+ s.textContent = await replaceBlobUrlsInCssText(css);
1242
+ }
1243
+ } catch {
1244
+ }
1245
+ }
1246
+ const urlAttrs = ["poster"];
1247
+ for (const attr of urlAttrs) {
1248
+ const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
1249
+ for (const n of nodes) {
1250
+ try {
1251
+ const u = n.getAttribute(attr);
1252
+ if (isBlobUrl(u)) {
1253
+ n.setAttribute(attr, await blobUrlToDataUrl(u));
1254
+ }
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ // src/core/clone.js
1122
1262
  async function deepClone(node, sessionCache, options) {
1123
1263
  if (!node) throw new Error("Invalid node");
1124
1264
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
@@ -1421,17 +1561,19 @@ var defaultIconFonts = [
1421
1561
  /layui/i,
1422
1562
  /lucide/i
1423
1563
  ];
1564
+ var ICON_FONT_URLS = Object.assign({
1565
+ materialIconsFilled: "https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2",
1566
+ materialIconsOutlined: "https://fonts.gstatic.com/s/materialiconsoutlined/v110/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUcel5euIg.woff2",
1567
+ materialIconsRound: "https://fonts.gstatic.com/s/materialiconsround/v109/LDItaoyNOAY6Uewc665JcIzCKsKc_M9flwmPq_HTTw.woff2",
1568
+ materialIconsSharp: "https://fonts.gstatic.com/s/materialiconssharp/v110/oPWQ_lt5nv4pWNJpghLP75WiFR4kLh3kvmvRImcycg.woff2"
1569
+ }, typeof window !== "undefined" && window.__SNAPDOM_ICON_FONTS__ || {});
1424
1570
  var userIconFonts = [];
1425
1571
  function extendIconFonts(fonts) {
1426
1572
  const list = Array.isArray(fonts) ? fonts : [fonts];
1427
1573
  for (const f of list) {
1428
- if (f instanceof RegExp) {
1429
- userIconFonts.push(f);
1430
- } else if (typeof f === "string") {
1431
- userIconFonts.push(new RegExp(f, "i"));
1432
- } else {
1433
- console.warn("[snapdom] Ignored invalid iconFont value:", f);
1434
- }
1574
+ if (f instanceof RegExp) userIconFonts.push(f);
1575
+ else if (typeof f === "string") userIconFonts.push(new RegExp(f, "i"));
1576
+ else console.warn("[snapdom] Ignored invalid iconFont value:", f);
1435
1577
  }
1436
1578
  }
1437
1579
  function isIconFont2(input) {
@@ -1447,10 +1589,62 @@ function isMaterialFamily(family = "") {
1447
1589
  const s = String(family).toLowerCase();
1448
1590
  return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
1449
1591
  }
1592
+ var loadedCanvasFamilies = /* @__PURE__ */ new Map();
1593
+ function parseAxes(variation = "") {
1594
+ const out = /* @__PURE__ */ Object.create(null);
1595
+ const v = String(variation || "");
1596
+ const rx = /['"]?\s*([A-Za-z]{3,4})\s*['"]?\s*([+-]?\d+(?:\.\d+)?)\s*/g;
1597
+ let m;
1598
+ while (m = rx.exec(v)) out[m[1].toUpperCase()] = Number(m[2]);
1599
+ return out;
1600
+ }
1601
+ async function ensureLigatureCanvasFont(cssFamily, className, axes) {
1602
+ const fam = String(cssFamily || "");
1603
+ const lowerFam = fam.toLowerCase();
1604
+ const cls = String(className || "").toLowerCase();
1605
+ if (/\bmaterial\s*icons\b/.test(lowerFam) && !/\bsymbols\b/.test(lowerFam)) {
1606
+ return { familyForMeasure: fam, familyForCanvas: fam };
1607
+ }
1608
+ const isSymbols = /\bmaterial\s*symbols\b/.test(lowerFam);
1609
+ if (!isSymbols) {
1610
+ return { familyForMeasure: fam, familyForCanvas: fam };
1611
+ }
1612
+ const FILL = axes && (axes.FILL ?? axes.fill);
1613
+ let style = "outlined";
1614
+ if (/\brounded\b/.test(cls) || /\bround\b/.test(cls)) style = "rounded";
1615
+ else if (/\bsharp\b/.test(cls)) style = "sharp";
1616
+ else if (/\boutlined\b/.test(cls)) style = "outlined";
1617
+ const filled = FILL === 1;
1618
+ let pick = null;
1619
+ if (filled) {
1620
+ if (style === "outlined" && ICON_FONT_URLS.materialIconsFilled) {
1621
+ pick = { url: ICON_FONT_URLS.materialIconsFilled, alias: "snapdom-mi-filled" };
1622
+ } else if (style === "rounded" && ICON_FONT_URLS.materialIconsRound) {
1623
+ pick = { url: ICON_FONT_URLS.materialIconsRound, alias: "snapdom-mi-round" };
1624
+ } else if (style === "sharp" && ICON_FONT_URLS.materialIconsSharp) {
1625
+ pick = { url: ICON_FONT_URLS.materialIconsSharp, alias: "snapdom-mi-sharp" };
1626
+ }
1627
+ }
1628
+ if (!pick) {
1629
+ return { familyForMeasure: fam, familyForCanvas: fam };
1630
+ }
1631
+ if (!loadedCanvasFamilies.has(pick.alias)) {
1632
+ try {
1633
+ const ff = new FontFace(pick.alias, `url(${pick.url})`, { style: "normal", weight: "400" });
1634
+ document.fonts.add(ff);
1635
+ await ff.load();
1636
+ loadedCanvasFamilies.set(pick.alias, true);
1637
+ } catch {
1638
+ return { familyForMeasure: fam, familyForCanvas: fam };
1639
+ }
1640
+ }
1641
+ const quoted = `"${pick.alias}"`;
1642
+ return { familyForMeasure: quoted, familyForCanvas: quoted };
1643
+ }
1450
1644
  async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1451
1645
  try {
1452
1646
  await Promise.all([
1453
- document.fonts.load(`400 ${px}px "${family.replace(/["']/g, "")}"`),
1647
+ document.fonts.load(`400 ${px}px "${String(family).replace(/["']/g, "")}"`),
1454
1648
  document.fonts.ready
1455
1649
  ]);
1456
1650
  } catch {
@@ -1458,10 +1652,8 @@ async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1458
1652
  }
1459
1653
  function resolvePaintColor(cs) {
1460
1654
  let fill = cs.getPropertyValue("-webkit-text-fill-color")?.trim() || "";
1461
- const isTransparent = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(fill);
1462
- if (fill && !isTransparent && fill.toLowerCase() !== "currentcolor") {
1463
- return fill;
1464
- }
1655
+ const isTransparent = /^transparent$/i.test(fill) || /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(fill);
1656
+ if (fill && !isTransparent && fill.toLowerCase() !== "currentcolor") return fill;
1465
1657
  const c = cs.color?.trim();
1466
1658
  return c && c !== "inherit" ? c : "#000";
1467
1659
  }
@@ -1470,18 +1662,21 @@ async function materialIconToImage(ligatureText, {
1470
1662
  weight = "normal",
1471
1663
  fontSize = 32,
1472
1664
  color = "#000",
1473
- variation = ""
1665
+ variation = "",
1666
+ className = ""
1474
1667
  } = {}) {
1475
1668
  const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
1476
1669
  const dpr = window.devicePixelRatio || 1;
1477
- await ensureMaterialFontsReady(fam, fontSize);
1670
+ const axes = parseAxes(variation);
1671
+ const { familyForMeasure, familyForCanvas } = await ensureLigatureCanvasFont(fam, className, axes);
1672
+ await ensureMaterialFontsReady(familyForCanvas.replace(/^["']+|["']+$/g, ""), fontSize);
1478
1673
  const span = document.createElement("span");
1479
1674
  span.textContent = ligatureText;
1480
1675
  span.style.position = "absolute";
1481
1676
  span.style.visibility = "hidden";
1482
1677
  span.style.left = "-99999px";
1483
1678
  span.style.whiteSpace = "nowrap";
1484
- span.style.fontFamily = `"${fam}"`;
1679
+ span.style.fontFamily = familyForMeasure;
1485
1680
  span.style.fontWeight = String(weight || "normal");
1486
1681
  span.style.fontSize = `${fontSize}px`;
1487
1682
  span.style.lineHeight = "1";
@@ -1489,7 +1684,6 @@ async function materialIconToImage(ligatureText, {
1489
1684
  span.style.padding = "0";
1490
1685
  span.style.fontFeatureSettings = "'liga' 1";
1491
1686
  span.style.fontVariantLigatures = "normal";
1492
- if (variation) span.style.fontVariationSettings = variation;
1493
1687
  span.style.color = color;
1494
1688
  document.body.appendChild(span);
1495
1689
  const rect = span.getBoundingClientRect();
@@ -1501,7 +1695,7 @@ async function materialIconToImage(ligatureText, {
1501
1695
  canvas.height = height * dpr;
1502
1696
  const ctx = canvas.getContext("2d");
1503
1697
  ctx.scale(dpr, dpr);
1504
- ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px "${fam}"`;
1698
+ ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px ${familyForCanvas}`;
1505
1699
  ctx.textAlign = "left";
1506
1700
  ctx.textBaseline = "top";
1507
1701
  ctx.fillStyle = color;
@@ -1529,21 +1723,23 @@ async function ligatureIconToImage(cloneRoot, sourceRoot) {
1529
1723
  const el = cloneNodes[i];
1530
1724
  const src = sourceNodes[i] || null;
1531
1725
  try {
1532
- const csSrc = src ? getComputedStyle(src) : getComputedStyle(el);
1533
- const family = csSrc.fontFamily || "Material Icons";
1726
+ const cs = src ? getComputedStyle(src) : getComputedStyle(el);
1727
+ const family = cs.fontFamily || "Material Icons";
1534
1728
  if (!isMaterialFamily(family)) continue;
1535
1729
  const text = (src || el).textContent.trim();
1536
1730
  if (!text) continue;
1537
- const size = parseInt(csSrc.fontSize, 10) || 24;
1538
- const weight = csSrc.fontWeight && csSrc.fontWeight !== "normal" ? csSrc.fontWeight : "normal";
1539
- const color = resolvePaintColor(csSrc);
1540
- const variation = csSrc.fontVariationSettings && csSrc.fontVariationSettings !== "normal" ? csSrc.fontVariationSettings : "";
1731
+ const size = parseInt(cs.fontSize, 10) || 24;
1732
+ const weight = cs.fontWeight && cs.fontWeight !== "normal" ? cs.fontWeight : "normal";
1733
+ const color = resolvePaintColor(cs);
1734
+ const variation = cs.fontVariationSettings && cs.fontVariationSettings !== "normal" ? cs.fontVariationSettings : "";
1735
+ const className = (src || el).className || "";
1541
1736
  const { dataUrl, width, height } = await materialIconToImage(text, {
1542
1737
  family,
1543
1738
  weight,
1544
1739
  fontSize: size,
1545
1740
  color,
1546
- variation
1741
+ variation,
1742
+ className
1547
1743
  });
1548
1744
  el.textContent = "";
1549
1745
  const img = el.ownerDocument.createElement("img");
@@ -1909,7 +2105,7 @@ function dedupeFontFaces(cssText) {
1909
2105
  const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1910
2106
  const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1911
2107
  const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1912
- const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
2108
+ const srcRaw = (block.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
1913
2109
  const urls = extractSrcUrls(srcRaw, location.href);
1914
2110
  const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
1915
2111
  const key = [
@@ -2117,7 +2313,7 @@ async function embedCustomFonts({
2117
2313
  const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
2118
2314
  const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
2119
2315
  const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
2120
- const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
2316
+ const srcRaw = (face.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
2121
2317
  const srcUrls = extractSrcUrls(srcRaw, link.href);
2122
2318
  if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
2123
2319
  const ranges = parseUnicodeRange(urange);
@@ -2527,6 +2723,152 @@ function resolveCountersInContent(raw, node, ctx) {
2527
2723
  }
2528
2724
 
2529
2725
  // src/modules/pseudo.js
2726
+ var __preflightMemo = /* @__PURE__ */ new WeakMap();
2727
+ var CSS_RULE_SCAN_BUDGET = 300;
2728
+ function preflightWithFp(doc, sessionCache) {
2729
+ const fp = styleFingerprint(doc);
2730
+ if (!sessionCache) return shouldProcessPseudos(doc);
2731
+ if (sessionCache.__pseudoPreflightFp !== fp) {
2732
+ sessionCache.__pseudoPreflight = shouldProcessPseudos(doc);
2733
+ sessionCache.__pseudoPreflightFp = fp;
2734
+ }
2735
+ return !!sessionCache.__pseudoPreflight;
2736
+ }
2737
+ function safeRules(sheet) {
2738
+ try {
2739
+ return sheet && sheet.cssRules ? sheet.cssRules : null;
2740
+ } catch {
2741
+ return null;
2742
+ }
2743
+ }
2744
+ function styleFingerprint(doc) {
2745
+ const nodes = doc.querySelectorAll('style,link[rel~="stylesheet"]');
2746
+ let fp = `n:${nodes.length}|`;
2747
+ let totalRules = 0;
2748
+ for (let i = 0; i < nodes.length; i++) {
2749
+ const n = nodes[i];
2750
+ if (n.tagName === "STYLE") {
2751
+ const len = n.textContent ? n.textContent.length : 0;
2752
+ fp += `S${len}|`;
2753
+ const sheet = (
2754
+ /** @type {HTMLStyleElement} */
2755
+ n.sheet
2756
+ );
2757
+ const rules = sheet ? safeRules(sheet) : null;
2758
+ if (rules) totalRules += rules.length;
2759
+ } else {
2760
+ const href = n.getAttribute("href") || "";
2761
+ const media = n.getAttribute("media") || "all";
2762
+ fp += `L${href}|m:${media}|`;
2763
+ const sheet = (
2764
+ /** @type {HTMLLinkElement} */
2765
+ n.sheet
2766
+ );
2767
+ const rules = sheet ? safeRules(sheet) : null;
2768
+ if (rules) totalRules += rules.length;
2769
+ }
2770
+ }
2771
+ const ass = (
2772
+ /** @type {any} */
2773
+ doc.adoptedStyleSheets
2774
+ );
2775
+ fp += `ass:${Array.isArray(ass) ? ass.length : 0}|tr:${totalRules}`;
2776
+ return fp;
2777
+ }
2778
+ function sheetHasNeedles(sheet, needles, state) {
2779
+ const rules = safeRules(sheet);
2780
+ if (!rules) return false;
2781
+ for (let i = 0; i < rules.length; i++) {
2782
+ if (state.budget <= 0) return false;
2783
+ const rule = rules[i];
2784
+ const css = rule && rule.cssText ? rule.cssText : "";
2785
+ state.budget--;
2786
+ for (const k of needles) {
2787
+ if (css.includes(k)) return true;
2788
+ }
2789
+ if (rule && rule.cssRules && rule.cssRules.length) {
2790
+ for (let j = 0; j < rule.cssRules.length && state.budget > 0; j++) {
2791
+ const inner = rule.cssRules[j];
2792
+ const innerCss = inner && inner.cssText ? inner.cssText : "";
2793
+ state.budget--;
2794
+ for (const k of needles) {
2795
+ if (innerCss.includes(k)) return true;
2796
+ }
2797
+ }
2798
+ }
2799
+ if (state.budget <= 0) return false;
2800
+ }
2801
+ return false;
2802
+ }
2803
+ function shouldProcessPseudos(doc = document) {
2804
+ const fp = styleFingerprint(doc);
2805
+ const memo = __preflightMemo.get(doc);
2806
+ if (memo && memo.fingerprint === fp) return memo.result;
2807
+ const NEEDLES = [
2808
+ // double-colon
2809
+ "::before",
2810
+ "::after",
2811
+ "::first-letter",
2812
+ // single-colon robustness
2813
+ ":before",
2814
+ ":after",
2815
+ ":first-letter",
2816
+ // counters
2817
+ "counter(",
2818
+ "counters(",
2819
+ "counter-increment",
2820
+ "counter-reset"
2821
+ ];
2822
+ const styleEls = doc.querySelectorAll("style");
2823
+ for (let i = 0; i < styleEls.length; i++) {
2824
+ const t = styleEls[i].textContent || "";
2825
+ for (const k of NEEDLES) if (t.includes(k)) {
2826
+ __preflightMemo.set(doc, { fingerprint: fp, result: true });
2827
+ return true;
2828
+ }
2829
+ }
2830
+ const ass = (
2831
+ /** @type {any} */
2832
+ doc.adoptedStyleSheets
2833
+ );
2834
+ if (Array.isArray(ass) && ass.length) {
2835
+ const state = { budget: CSS_RULE_SCAN_BUDGET };
2836
+ try {
2837
+ for (const sheet of ass) {
2838
+ if (sheetHasNeedles(sheet, NEEDLES, state)) {
2839
+ __preflightMemo.set(doc, { fingerprint: fp, result: true });
2840
+ return true;
2841
+ }
2842
+ }
2843
+ } catch {
2844
+ }
2845
+ }
2846
+ {
2847
+ const nodes = doc.querySelectorAll('style,link[rel~="stylesheet"]');
2848
+ const state = { budget: CSS_RULE_SCAN_BUDGET };
2849
+ for (let i = 0; i < nodes.length && state.budget > 0; i++) {
2850
+ const n = nodes[i];
2851
+ let sheet = null;
2852
+ if (n.tagName === "STYLE") {
2853
+ sheet = /** @type {HTMLStyleElement} */
2854
+ n.sheet || null;
2855
+ } else {
2856
+ sheet = /** @type {HTMLLinkElement} */
2857
+ n.sheet || null;
2858
+ }
2859
+ if (sheet && sheetHasNeedles(sheet, NEEDLES, state)) {
2860
+ __preflightMemo.set(doc, { fingerprint: fp, result: true });
2861
+ return true;
2862
+ }
2863
+ }
2864
+ }
2865
+ if (doc.querySelector('[style*="counter("], [style*="counters("]')) {
2866
+ __preflightMemo.set(doc, { fingerprint: fp, result: true });
2867
+ return true;
2868
+ }
2869
+ __preflightMemo.set(doc, { fingerprint: fp, result: false });
2870
+ return false;
2871
+ }
2530
2872
  var __siblingCounters = /* @__PURE__ */ new WeakMap();
2531
2873
  var __pseudoEpoch = -1;
2532
2874
  function unquoteDoubleStrings2(s) {
@@ -2625,6 +2967,10 @@ function resolvePseudoContentAndIncs(node, pseudo, baseCtx) {
2625
2967
  }
2626
2968
  async function inlinePseudoElements(source, clone, sessionCache, options) {
2627
2969
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
2970
+ const doc = source.ownerDocument || document;
2971
+ if (!preflightWithFp(doc, sessionCache)) {
2972
+ return;
2973
+ }
2628
2974
  const epoch = cache?.session?.__counterEpoch ?? 0;
2629
2975
  if (__pseudoEpoch !== epoch) {
2630
2976
  __siblingCounters = /* @__PURE__ */ new WeakMap();
@@ -2641,7 +2987,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2641
2987
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
2642
2988
  try {
2643
2989
  const style = getStyle(source, pseudo);
2644
- if (!style || typeof style[Symbol.iterator] !== "function") continue;
2990
+ if (!style) continue;
2645
2991
  const isEmptyPseudo = style.content === "none" && style.backgroundImage === "none" && style.backgroundColor === "transparent" && (style.borderStyle === "none" || parseFloat(style.borderWidth) === 0) && (!style.transform || style.transform === "none") && style.display === "inline";
2646
2992
  if (isEmptyPseudo) continue;
2647
2993
  if (pseudo === "::first-letter") {
@@ -2668,7 +3014,8 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2668
3014
  clone.insertBefore(span, restNode);
2669
3015
  continue;
2670
3016
  }
2671
- const rawContent = style.content;
3017
+ const rawContent = style.content ?? "";
3018
+ const isNoExplicitContent = rawContent === "" || rawContent === "none" || rawContent === "normal";
2672
3019
  const { text: cleanContent, incs } = resolvePseudoContentAndIncs(source, pseudo, counterCtx);
2673
3020
  const bg = style.backgroundImage;
2674
3021
  const bgColor = style.backgroundColor;
@@ -2680,7 +3027,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2680
3027
  const borderWidth = parseFloat(style.borderWidth);
2681
3028
  const transform = style.transform;
2682
3029
  const isIconFont22 = isIconFont2(fontFamily);
2683
- const hasExplicitContent = rawContent !== "none" && cleanContent !== "";
3030
+ const hasExplicitContent = !isNoExplicitContent && cleanContent !== "";
2684
3031
  const hasBg = bg && bg !== "none";
2685
3032
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
2686
3033
  const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
@@ -2702,8 +3049,6 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2702
3049
  }
2703
3050
  const pseudoEl = document.createElement("span");
2704
3051
  pseudoEl.dataset.snapdomPseudo = pseudo;
2705
- pseudoEl.style.display = "inline";
2706
- pseudoEl.style.verticalAlign = "baseline";
2707
3052
  pseudoEl.style.pointerEvents = "none";
2708
3053
  const snapshot = snapshotComputedStyle(style);
2709
3054
  const key = getStyleKey(snapshot, "span");
@@ -2995,6 +3340,20 @@ function _childrenWithoutPlaceholders(el, phAttr) {
2995
3340
  return out;
2996
3341
  }
2997
3342
 
3343
+ // src/utils/prepare.helpers.js
3344
+ function stabilizeLayout(element) {
3345
+ const style = getComputedStyle(element);
3346
+ const outlineStyle = style.outlineStyle;
3347
+ const outlineWidth = style.outlineWidth;
3348
+ const borderStyle = style.borderStyle;
3349
+ const borderWidth = style.borderWidth;
3350
+ const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
3351
+ const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
3352
+ if (outlineVisible && borderAbsent) {
3353
+ element.style.border = `${outlineWidth} solid transparent`;
3354
+ }
3355
+ }
3356
+
2998
3357
  // src/core/prepare.js
2999
3358
  async function prepareClone(element, options = {}) {
3000
3359
  const sessionCache = {
@@ -3095,152 +3454,25 @@ async function prepareClone(element, options = {}) {
3095
3454
  }
3096
3455
  return { clone, classCSS, styleCache: sessionCache.styleCache };
3097
3456
  }
3098
- function stabilizeLayout(element) {
3099
- const style = getComputedStyle(element);
3100
- const outlineStyle = style.outlineStyle;
3101
- const outlineWidth = style.outlineWidth;
3102
- const borderStyle = style.borderStyle;
3103
- const borderWidth = style.borderWidth;
3104
- const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
3105
- const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
3106
- if (outlineVisible && borderAbsent) {
3107
- element.style.border = `${outlineWidth} solid transparent`;
3108
- }
3109
- }
3110
- var _blobToDataUrlCache = /* @__PURE__ */ new Map();
3111
- async function blobUrlToDataUrl(blobUrl) {
3112
- if (cache.resource?.has(blobUrl)) return cache.resource.get(blobUrl);
3113
- if (_blobToDataUrlCache.has(blobUrl)) return _blobToDataUrlCache.get(blobUrl);
3114
- const p = (async () => {
3115
- const r = await snapFetch(blobUrl, { as: "dataURL", silent: true });
3116
- if (!r.ok || typeof r.data !== "string") {
3117
- throw new Error(`[snapDOM] Failed to read blob URL: ${blobUrl}`);
3118
- }
3119
- cache.resource?.set(blobUrl, r.data);
3120
- return r.data;
3121
- })();
3122
- _blobToDataUrlCache.set(blobUrl, p);
3123
- try {
3124
- const data = await p;
3125
- _blobToDataUrlCache.set(blobUrl, data);
3126
- return data;
3127
- } catch (e) {
3128
- _blobToDataUrlCache.delete(blobUrl);
3129
- throw e;
3130
- }
3457
+
3458
+ // src/modules/images.js
3459
+ function extractImageDimensions(img) {
3460
+ const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
3461
+ const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
3462
+ const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
3463
+ const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
3464
+ const styleW = parseFloat(img.style?.width || "") || 0;
3465
+ const styleH = parseFloat(img.style?.height || "") || 0;
3466
+ const w = dsW || styleW || attrW || img.width || img.naturalWidth || 100;
3467
+ const h = dsH || styleH || attrH || img.height || img.naturalHeight || 100;
3468
+ return { width: w, height: h };
3131
3469
  }
3132
- var BLOB_URL_RE = /\bblob:[^)"'\s]+/g;
3133
- async function replaceBlobUrlsInCssText(cssText) {
3134
- if (!cssText || cssText.indexOf("blob:") === -1) return cssText;
3135
- const uniques = Array.from(new Set(cssText.match(BLOB_URL_RE) || []));
3136
- if (uniques.length === 0) return cssText;
3137
- let out = cssText;
3138
- for (const u of uniques) {
3139
- try {
3140
- const d = await blobUrlToDataUrl(u);
3141
- out = out.split(u).join(d);
3142
- } catch {
3143
- }
3144
- }
3145
- return out;
3146
- }
3147
- function isBlobUrl(u) {
3148
- return typeof u === "string" && u.startsWith("blob:");
3149
- }
3150
- function parseSrcset(srcset) {
3151
- return (srcset || "").split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
3152
- const m = item.match(/^(\S+)(\s+.+)?$/);
3153
- return m ? { url: m[1], desc: m[2] || "" } : null;
3154
- }).filter(Boolean);
3155
- }
3156
- function stringifySrcset(parts) {
3157
- return parts.map((p) => p.desc ? `${p.url} ${p.desc.trim()}` : p.url).join(", ");
3158
- }
3159
- async function resolveBlobUrlsInTree(root) {
3160
- if (!root) return;
3161
- const imgs = root.querySelectorAll ? root.querySelectorAll("img") : [];
3162
- for (const img of imgs) {
3163
- try {
3164
- const srcAttr = img.getAttribute("src");
3165
- const effective = srcAttr || img.currentSrc || "";
3166
- if (isBlobUrl(effective)) {
3167
- const data = await blobUrlToDataUrl(effective);
3168
- img.setAttribute("src", data);
3169
- }
3170
- const srcset = img.getAttribute("srcset");
3171
- if (srcset && srcset.includes("blob:")) {
3172
- const parts = parseSrcset(srcset);
3173
- let changed = false;
3174
- for (const p of parts) {
3175
- if (isBlobUrl(p.url)) {
3176
- try {
3177
- p.url = await blobUrlToDataUrl(p.url);
3178
- changed = true;
3179
- } catch {
3180
- }
3181
- }
3182
- }
3183
- if (changed) img.setAttribute("srcset", stringifySrcset(parts));
3184
- }
3185
- } catch {
3186
- }
3187
- }
3188
- const svgImages = root.querySelectorAll ? root.querySelectorAll("image") : [];
3189
- for (const node of svgImages) {
3190
- try {
3191
- const XLINK_NS = "http://www.w3.org/1999/xlink";
3192
- const href = node.getAttribute("href") || node.getAttributeNS?.(XLINK_NS, "href");
3193
- if (isBlobUrl(href)) {
3194
- const d = await blobUrlToDataUrl(href);
3195
- node.setAttribute("href", d);
3196
- node.removeAttributeNS?.(XLINK_NS, "href");
3197
- }
3198
- } catch {
3199
- }
3200
- }
3201
- const styled = root.querySelectorAll ? root.querySelectorAll("[style*='blob:']") : [];
3202
- for (const el of styled) {
3203
- try {
3204
- const styleText = el.getAttribute("style");
3205
- if (styleText && styleText.includes("blob:")) {
3206
- const replaced = await replaceBlobUrlsInCssText(styleText);
3207
- el.setAttribute("style", replaced);
3208
- }
3209
- } catch {
3210
- }
3211
- }
3212
- const styleTags = root.querySelectorAll ? root.querySelectorAll("style") : [];
3213
- for (const s of styleTags) {
3214
- try {
3215
- const css = s.textContent || "";
3216
- if (css.includes("blob:")) {
3217
- s.textContent = await replaceBlobUrlsInCssText(css);
3218
- }
3219
- } catch {
3220
- }
3221
- }
3222
- const urlAttrs = ["poster"];
3223
- for (const attr of urlAttrs) {
3224
- const nodes = root.querySelectorAll ? root.querySelectorAll(`[${attr}^='blob:']`) : [];
3225
- for (const n of nodes) {
3226
- try {
3227
- const u = n.getAttribute(attr);
3228
- if (isBlobUrl(u)) {
3229
- n.setAttribute(attr, await blobUrlToDataUrl(u));
3230
- }
3231
- } catch {
3232
- }
3233
- }
3234
- }
3235
- }
3236
-
3237
- // src/modules/images.js
3238
- async function inlineImages(clone, options = {}) {
3239
- const imgs = Array.from(clone.querySelectorAll("img"));
3240
- const processImg = async (img) => {
3241
- if (!img.getAttribute("src")) {
3242
- const eff = img.currentSrc || img.src || "";
3243
- if (eff) img.setAttribute("src", eff);
3470
+ async function inlineImages(clone, options = {}) {
3471
+ const imgs = Array.from(clone.querySelectorAll("img"));
3472
+ const processImg = async (img) => {
3473
+ if (!img.getAttribute("src")) {
3474
+ const eff = img.currentSrc || img.src || "";
3475
+ if (eff) img.setAttribute("src", eff);
3244
3476
  }
3245
3477
  img.removeAttribute("srcset");
3246
3478
  img.removeAttribute("sizes");
@@ -3253,41 +3485,30 @@ async function inlineImages(clone, options = {}) {
3253
3485
  if (!img.height) img.height = img.naturalHeight || 100;
3254
3486
  return;
3255
3487
  }
3488
+ const { width: fbW, height: fbH } = extractImageDimensions(img);
3256
3489
  const { fallbackURL } = options || {};
3257
3490
  if (fallbackURL) {
3258
3491
  try {
3259
- const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
3260
- const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
3261
- const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
3262
- const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
3263
- const styleW = parseFloat(img.style?.width || "") || 0;
3264
- const styleH = parseFloat(img.style?.height || "") || 0;
3265
- const fbW = dsW || styleW || attrW || img.width || void 0;
3266
- const fbH = dsH || styleH || attrH || img.height || void 0;
3267
3492
  const fallbackUrl = typeof fallbackURL === "function" ? await fallbackURL({ width: fbW, height: fbH, src, element: img }) : fallbackURL;
3268
3493
  if (fallbackUrl) {
3269
3494
  const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
3270
3495
  img.src = fallbackData.data;
3271
- if (!img.width && fbW) img.width = fbW;
3272
- if (!img.height && fbH) img.height = fbH;
3273
- if (!img.width) img.width = img.naturalWidth || 100;
3274
- if (!img.height) img.height = img.naturalHeight || 100;
3496
+ if (!img.width) img.width = fbW;
3497
+ if (!img.height) img.height = fbH;
3275
3498
  return;
3276
3499
  }
3277
3500
  } catch {
3278
3501
  }
3279
3502
  }
3280
- const w = img.width || img.naturalWidth || 100;
3281
- const h = img.height || img.naturalHeight || 100;
3282
3503
  if (options.placeholders !== false) {
3283
3504
  const fallback = document.createElement("div");
3284
3505
  fallback.style.cssText = [
3285
- `width:${w}px`,
3286
- `height:${h}px`,
3506
+ `width:${fbW}px`,
3507
+ `height:${fbH}px`,
3287
3508
  "background:#ccc",
3288
3509
  "display:inline-block",
3289
3510
  "text-align:center",
3290
- `line-height:${h}px`,
3511
+ `line-height:${fbH}px`,
3291
3512
  "color:#666",
3292
3513
  "font-size:12px",
3293
3514
  "overflow:hidden"
@@ -3296,7 +3517,7 @@ async function inlineImages(clone, options = {}) {
3296
3517
  img.replaceWith(fallback);
3297
3518
  } else {
3298
3519
  const spacer = document.createElement("div");
3299
- spacer.style.cssText = `display:inline-block;width:${w}px;height:${h}px;visibility:hidden;`;
3520
+ spacer.style.cssText = `display:inline-block;width:${fbW}px;height:${fbH}px;visibility:hidden;`;
3300
3521
  img.replaceWith(spacer);
3301
3522
  }
3302
3523
  };
@@ -3536,7 +3757,7 @@ function attachSessionPlugins(context, localDefs, force = false) {
3536
3757
  return context;
3537
3758
  }
3538
3759
 
3539
- // src/core/capture.js
3760
+ // src/utils/capture.helpers.js
3540
3761
  function stripRootShadows(originalEl, cloneRoot) {
3541
3762
  if (!originalEl || !cloneRoot || !cloneRoot.style) return;
3542
3763
  const cs = getComputedStyle(originalEl);
@@ -3559,217 +3780,178 @@ function stripRootShadows(originalEl, cloneRoot) {
3559
3780
  } catch {
3560
3781
  }
3561
3782
  }
3562
- async function captureDOM(element, options) {
3563
- if (!element) throw new Error("Element cannot be null or undefined");
3564
- applyCachePolicy(options.cache);
3565
- const fast = options.fast;
3566
- const outerTransforms = options.outerTransforms !== false;
3567
- const outerShadows = !!options.outerShadows;
3568
- let state = { element, options, plugins: options.plugins };
3569
- let clone, classCSS, styleCache;
3570
- let fontsCSS = "";
3571
- let baseCSS = "";
3572
- let dataURL;
3573
- let svgString;
3574
- let rootTransform2D = null;
3575
- await runHook("beforeSnap", state);
3576
- await runHook("beforeClone", state);
3577
- const undoClamp = lineClamp(state.element);
3578
- try {
3579
- ({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
3580
- if (!outerTransforms && clone) {
3581
- rootTransform2D = normalizeRootTransforms(state.element, clone);
3582
- }
3583
- if (!outerShadows && clone) {
3584
- stripRootShadows(state.element, clone);
3783
+ function removeAllComments(root) {
3784
+ const it = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
3785
+ const toRemove = [];
3786
+ while (it.nextNode()) toRemove.push(it.currentNode);
3787
+ for (const n of toRemove) n.remove();
3788
+ }
3789
+ function sanitizeAttributesForXHTML(root, opts = {}) {
3790
+ const { stripFrameworkDirectives = true } = opts;
3791
+ const ALLOWED_PREFIXES = /* @__PURE__ */ new Set(["xml", "xlink"]);
3792
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
3793
+ while (walker.nextNode()) {
3794
+ const el = walker.currentNode;
3795
+ for (const attr of Array.from(el.attributes)) {
3796
+ const name = attr.name;
3797
+ if (name.includes("@")) {
3798
+ el.removeAttribute(name);
3799
+ continue;
3800
+ }
3801
+ if (name.includes(":")) {
3802
+ const prefix = name.split(":", 1)[0];
3803
+ if (!ALLOWED_PREFIXES.has(prefix)) {
3804
+ el.removeAttribute(name);
3805
+ continue;
3806
+ }
3807
+ }
3808
+ if (!stripFrameworkDirectives) continue;
3809
+ if (name.startsWith("x-") || // Alpine
3810
+ name.startsWith("v-") || // Vue
3811
+ name.startsWith(":") || // Vue/Alpine shorthand
3812
+ name.startsWith("on:") || // Svelte
3813
+ name.startsWith("bind:") || // Svelte
3814
+ name.startsWith("let:") || // Svelte
3815
+ name.startsWith("class:")) {
3816
+ el.removeAttribute(name);
3817
+ continue;
3818
+ }
3585
3819
  }
3586
- } finally {
3587
- undoClamp();
3588
3820
  }
3589
- state = { clone, classCSS, styleCache, ...state };
3590
- await runHook("afterClone", state);
3821
+ }
3822
+ function sanitizeCloneForXHTML(root, opts = {}) {
3823
+ if (!root) return;
3824
+ sanitizeAttributesForXHTML(root, opts);
3825
+ removeAllComments(root);
3826
+ }
3827
+ function authorHasExplicitSize(el) {
3591
3828
  try {
3592
- await ligatureIconToImage(state.clone, state.element);
3829
+ const s = el.getAttribute?.("style") || "";
3830
+ return /\b(height|width|block-size|inline-size)\s*:/.test(s);
3593
3831
  } catch {
3832
+ return false;
3594
3833
  }
3595
- await new Promise((resolve) => {
3596
- idle(async () => {
3597
- await inlineImages(state.clone, state.options);
3598
- resolve();
3599
- }, { fast });
3600
- });
3601
- await new Promise((resolve) => {
3602
- idle(async () => {
3603
- await inlineBackgroundImages(state.element, state.clone, state.styleCache, state.options);
3604
- resolve();
3605
- }, { fast });
3606
- });
3607
- if (options.embedFonts) {
3608
- await new Promise((resolve) => {
3609
- idle(async () => {
3610
- const required = collectUsedFontVariants(state.element);
3611
- const usedCodepoints = collectUsedCodepoints(state.element);
3612
- if (isSafari()) {
3613
- const families = new Set(
3614
- Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
3615
- );
3616
- await ensureFontsReady(families, 1);
3617
- }
3618
- fontsCSS = await embedCustomFonts({
3619
- required,
3620
- usedCodepoints,
3621
- preCached: false,
3622
- exclude: state.options.excludeFonts,
3623
- useProxy: state.options.useProxy
3624
- });
3625
- resolve();
3626
- }, { fast });
3627
- });
3628
- }
3629
- const usedTags = collectUsedTagNames(state.clone).sort();
3630
- const tagKey = usedTags.join(",");
3631
- if (cache.baseStyle.has(tagKey)) {
3632
- baseCSS = cache.baseStyle.get(tagKey);
3633
- } else {
3634
- await new Promise((resolve) => {
3635
- idle(() => {
3636
- baseCSS = generateDedupedBaseCSS(usedTags);
3637
- cache.baseStyle.set(tagKey, baseCSS);
3638
- resolve();
3639
- }, { fast });
3640
- });
3834
+ }
3835
+ function isReplacedElement(el) {
3836
+ return el instanceof HTMLImageElement || el instanceof HTMLCanvasElement || el instanceof HTMLVideoElement || el instanceof HTMLIFrameElement || el instanceof SVGElement || el instanceof HTMLObjectElement || el instanceof HTMLEmbedElement;
3837
+ }
3838
+ function shouldShrinkBox(srcEl, cs) {
3839
+ if (!(srcEl instanceof Element)) return false;
3840
+ if (authorHasExplicitSize(srcEl)) return false;
3841
+ if (isReplacedElement(srcEl)) return false;
3842
+ const pos = cs.position;
3843
+ if (pos === "absolute" || pos === "fixed" || pos === "sticky") return false;
3844
+ const disp = cs.display || "";
3845
+ if (disp.includes("flex") || disp.includes("grid") || disp.startsWith("table")) return false;
3846
+ if (cs.transform && cs.transform !== "none") return false;
3847
+ return true;
3848
+ }
3849
+ function shrinkAutoSizeBoxes(sourceRoot, cloneRoot, styleCache = /* @__PURE__ */ new Map()) {
3850
+ function walk(src, cln) {
3851
+ if (!(src instanceof Element) || !(cln instanceof Element)) return;
3852
+ const lostKids = src.childElementCount > cln.childElementCount;
3853
+ const cs = styleCache.get(src) || getComputedStyle(src);
3854
+ if (!styleCache.has(src)) styleCache.set(src, cs);
3855
+ if (lostKids && shouldShrinkBox(src, cs)) {
3856
+ if (!cln.style.height) cln.style.height = "auto";
3857
+ if (!cln.style.width) cln.style.width = "auto";
3858
+ cln.style.removeProperty("block-size");
3859
+ cln.style.removeProperty("inline-size");
3860
+ if (!cln.style.minHeight) cln.style.minHeight = "0";
3861
+ if (!cln.style.minWidth) cln.style.minWidth = "0";
3862
+ if (!cln.style.maxHeight) cln.style.maxHeight = "none";
3863
+ if (!cln.style.maxWidth) cln.style.maxWidth = "none";
3864
+ const oy = cs.overflowY || cs.overflowBlock || "visible";
3865
+ const ox = cs.overflowX || cs.overflowInline || "visible";
3866
+ if (oy !== "visible" || ox !== "visible") {
3867
+ cln.style.overflow = "visible";
3868
+ }
3869
+ }
3870
+ const sKids = Array.from(src.children);
3871
+ const cKids = Array.from(cln.children);
3872
+ for (let i = 0; i < Math.min(sKids.length, cKids.length); i++) {
3873
+ walk(sKids[i], cKids[i]);
3874
+ }
3641
3875
  }
3642
- state = { fontsCSS, baseCSS, ...state };
3643
- await runHook("beforeRender", state);
3644
- await new Promise((resolve) => {
3645
- idle(() => {
3646
- const csEl = getComputedStyle(state.element);
3647
- const rect = state.element.getBoundingClientRect();
3648
- const w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3649
- const h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
3650
- const coerceNum = (v, def = NaN) => {
3651
- const n = typeof v === "string" ? parseFloat(v) : v;
3652
- return Number.isFinite(n) ? n : def;
3653
- };
3654
- const optW = coerceNum(state.options.width);
3655
- const optH = coerceNum(state.options.height);
3656
- let w = w0, h = h0;
3657
- const hasW = Number.isFinite(optW);
3658
- const hasH = Number.isFinite(optH);
3659
- const aspect0 = h0 > 0 ? w0 / h0 : 1;
3660
- if (hasW && hasH) {
3661
- w = Math.max(1, limitDecimals(optW));
3662
- h = Math.max(1, limitDecimals(optH));
3663
- } else if (hasW) {
3664
- w = Math.max(1, limitDecimals(optW));
3665
- h = Math.max(1, limitDecimals(w / (aspect0 || 1)));
3666
- } else if (hasH) {
3667
- h = Math.max(1, limitDecimals(optH));
3668
- w = Math.max(1, limitDecimals(h * (aspect0 || 1)));
3669
- } else {
3670
- w = w0;
3671
- h = h0;
3876
+ walk(sourceRoot, cloneRoot);
3877
+ }
3878
+ function isInNormalFlow(el) {
3879
+ const cs = getComputedStyle(el);
3880
+ if (cs.display === "none") return false;
3881
+ if (cs.position === "absolute" || cs.position === "fixed" || cs.position === "sticky") return false;
3882
+ if ((cs.cssFloat || cs.float || "none") !== "none") return false;
3883
+ if (cs.transform && cs.transform !== "none") return false;
3884
+ return true;
3885
+ }
3886
+ function willBeExcluded(el, options) {
3887
+ if (!(el instanceof Element)) return false;
3888
+ if (el.getAttribute("data-capture") === "exclude" && options?.excludeMode === "remove") return true;
3889
+ if (Array.isArray(options?.exclude)) {
3890
+ for (const sel of options.exclude) {
3891
+ try {
3892
+ if (el.matches(sel)) return options.excludeMode === "remove";
3893
+ } catch {
3672
3894
  }
3673
- let minX = 0, minY = 0, maxX = w0, maxY = h0;
3674
- if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3675
- const M2 = {
3676
- a: rootTransform2D.a,
3677
- b: rootTransform2D.b || 0,
3678
- c: rootTransform2D.c || 0,
3679
- d: rootTransform2D.d || 1,
3680
- e: 0,
3681
- f: 0
3682
- };
3683
- const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
3684
- minX = limitDecimals(bb2.minX);
3685
- minY = limitDecimals(bb2.minY);
3686
- maxX = limitDecimals(bb2.maxX);
3687
- maxY = limitDecimals(bb2.maxY);
3688
- } else {
3689
- const useTFBBox = outerTransforms && hasTFBBox(state.element);
3690
- if (useTFBBox) {
3691
- const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3692
- const ind2 = readIndividualTransforms(state.element);
3693
- const TOTAL = readTotalTransformMatrix({
3694
- baseTransform: baseTransform2,
3695
- rotate: ind2.rotate || "0deg",
3696
- scale: ind2.scale,
3697
- translate: ind2.translate
3698
- });
3699
- const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
3700
- const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
3701
- const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
3702
- minX = limitDecimals(bb.minX);
3703
- minY = limitDecimals(bb.minY);
3704
- maxX = limitDecimals(bb.maxX);
3705
- maxY = limitDecimals(bb.maxY);
3706
- }
3707
- }
3708
- const bleedShadow = parseBoxShadow(csEl);
3709
- const bleedBlur = parseFilterBlur(csEl);
3710
- const bleedOutline = parseOutline(csEl);
3711
- const drop = parseFilterDropShadows(csEl);
3712
- const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3713
- top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
3714
- right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
3715
- bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
3716
- left: limitDecimals(bleedShadow.left + bleedBlur.left + bleedOutline.left + drop.bleed.left)
3717
- };
3718
- minX = limitDecimals(minX - bleed.left);
3719
- minY = limitDecimals(minY - bleed.top);
3720
- maxX = limitDecimals(maxX + bleed.right);
3721
- maxY = limitDecimals(maxY + bleed.bottom);
3722
- const vbW0 = Math.max(1, limitDecimals(maxX - minX));
3723
- const vbH0 = Math.max(1, limitDecimals(maxY - minY));
3724
- const scaleW = hasW || hasH ? limitDecimals(w / w0) : 1;
3725
- const scaleH = hasH || hasW ? limitDecimals(h / h0) : 1;
3726
- const outW = Math.max(1, limitDecimals(vbW0 * scaleW));
3727
- const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
3728
- const svgNS = "http://www.w3.org/2000/svg";
3729
- const basePad = isSafari() ? 1 : 0;
3730
- const extraPad = !outerTransforms ? 1 : 0;
3731
- const pad = limitDecimals(basePad + extraPad);
3732
- const fo = document.createElementNS(svgNS, "foreignObject");
3733
- const vbMinX = limitDecimals(minX);
3734
- const vbMinY = limitDecimals(minY);
3735
- fo.setAttribute("x", String(limitDecimals(-(vbMinX - pad))));
3736
- fo.setAttribute("y", String(limitDecimals(-(vbMinY - pad))));
3737
- fo.setAttribute("width", String(limitDecimals(w0 + pad * 2)));
3738
- fo.setAttribute("height", String(limitDecimals(h0 + pad * 2)));
3739
- fo.style.overflow = "visible";
3740
- const styleTag = document.createElement("style");
3741
- styleTag.textContent = state.baseCSS + state.fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + state.classCSS;
3742
- fo.appendChild(styleTag);
3743
- const container = document.createElement("div");
3744
- container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3745
- container.style.width = `${limitDecimals(w0)}px`;
3746
- container.style.height = `${limitDecimals(h0)}px`;
3747
- container.style.overflow = "visible";
3748
- state.clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3749
- container.appendChild(state.clone);
3750
- fo.appendChild(container);
3751
- const serializer = new XMLSerializer();
3752
- const foString = serializer.serializeToString(fo);
3753
- const vbW = limitDecimals(vbW0 + pad * 2);
3754
- const vbH = limitDecimals(vbH0 + pad * 2);
3755
- const wantsSize = hasW || hasH;
3756
- options.meta = { w0, h0, vbW, vbH, targetW: w, targetH: h };
3757
- const svgOutW = isSafari() && wantsSize ? vbW : limitDecimals(outW + pad * 2);
3758
- const svgOutH = isSafari() && wantsSize ? vbH : limitDecimals(outH + pad * 2);
3759
- const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
3760
- const svgFooter = "</svg>";
3761
- svgString = svgHeader + foString + svgFooter;
3762
- dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
3763
- state = { svgString, dataURL, ...state };
3764
- resolve();
3765
- }, { fast });
3766
- });
3767
- await runHook("afterRender", state);
3768
- const sandbox = document.getElementById("snapdom-sandbox");
3769
- if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3770
- return state.dataURL;
3895
+ }
3896
+ }
3897
+ return false;
3898
+ }
3899
+ function estimateKeptHeight(container, options) {
3900
+ const csC = getComputedStyle(container);
3901
+ const rC = container.getBoundingClientRect();
3902
+ let minTop = Infinity;
3903
+ let maxBottom = -Infinity;
3904
+ let found = false;
3905
+ const kids = Array.from(container.children);
3906
+ for (const k of kids) {
3907
+ if (willBeExcluded(k, options)) continue;
3908
+ if (!isInNormalFlow(k)) continue;
3909
+ const rk = k.getBoundingClientRect();
3910
+ const top = rk.top - rC.top;
3911
+ const bottom = rk.bottom - rC.top;
3912
+ if (bottom <= top) continue;
3913
+ if (top < minTop) minTop = top;
3914
+ if (bottom > maxBottom) maxBottom = bottom;
3915
+ found = true;
3916
+ }
3917
+ const contentSpan = found ? Math.max(0, maxBottom - minTop) : 0;
3918
+ const bt = parseFloat(csC.borderTopWidth) || 0;
3919
+ const bb = parseFloat(csC.borderBottomWidth) || 0;
3920
+ const pt = parseFloat(csC.paddingTop) || 0;
3921
+ const pb = parseFloat(csC.paddingBottom) || 0;
3922
+ return bt + bb + pt + pb + contentSpan;
3771
3923
  }
3772
3924
  var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
3925
+
3926
+ // src/utils/transforms.helpers.js
3927
+ function parseBoxShadow(cs) {
3928
+ const v = cs.boxShadow || "";
3929
+ if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3930
+ const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
3931
+ let t = 0, r = 0, b2 = 0, l = 0;
3932
+ for (const part of parts) {
3933
+ const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
3934
+ if (nums.length < 2) continue;
3935
+ const [ox2, oy2, blur = 0, spread = 0] = nums;
3936
+ const extX = Math.abs(ox2) + blur + spread;
3937
+ const extY = Math.abs(oy2) + blur + spread;
3938
+ r = Math.max(r, extX + Math.max(ox2, 0));
3939
+ l = Math.max(l, extX + Math.max(-ox2, 0));
3940
+ b2 = Math.max(b2, extY + Math.max(oy2, 0));
3941
+ t = Math.max(t, extY + Math.max(-oy2, 0));
3942
+ }
3943
+ return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
3944
+ }
3945
+ function parseFilterBlur(cs) {
3946
+ const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
3947
+ const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
3948
+ return { top: b2, right: b2, bottom: b2, left: b2 };
3949
+ }
3950
+ function parseOutline(cs) {
3951
+ if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3952
+ const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
3953
+ return { top: w2, right: w2, bottom: w2, left: w2 };
3954
+ }
3773
3955
  function parseFilterDropShadows(cs) {
3774
3956
  const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
3775
3957
  if (!raw || raw === "none") {
@@ -3858,34 +4040,6 @@ function normalizeRootTransforms(originalEl, cloneRoot) {
3858
4040
  return null;
3859
4041
  }
3860
4042
  }
3861
- function parseBoxShadow(cs) {
3862
- const v = cs.boxShadow || "";
3863
- if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3864
- const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
3865
- let t = 0, r = 0, b2 = 0, l = 0;
3866
- for (const part of parts) {
3867
- const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
3868
- if (nums.length < 2) continue;
3869
- const [ox2, oy2, blur = 0, spread = 0] = nums;
3870
- const extX = Math.abs(ox2) + blur + spread;
3871
- const extY = Math.abs(oy2) + blur + spread;
3872
- r = Math.max(r, extX + Math.max(ox2, 0));
3873
- l = Math.max(l, extX + Math.max(-ox2, 0));
3874
- b2 = Math.max(b2, extY + Math.max(oy2, 0));
3875
- t = Math.max(t, extY + Math.max(-oy2, 0));
3876
- }
3877
- return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
3878
- }
3879
- function parseFilterBlur(cs) {
3880
- const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
3881
- const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
3882
- return { top: b2, right: b2, bottom: b2, left: b2 };
3883
- }
3884
- function parseOutline(cs) {
3885
- if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3886
- const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
3887
- return { top: w2, right: w2, bottom: w2, left: w2 };
3888
- }
3889
4043
  function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
3890
4044
  const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
3891
4045
  function pt(x, y) {
@@ -3905,17 +4059,24 @@ function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
3905
4059
  }
3906
4060
  return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
3907
4061
  }
3908
- function hasTFBBox(el) {
3909
- return hasBBoxAffectingTransform(el);
3910
- }
3911
- function matrixFromComputed(el) {
3912
- const tr = getComputedStyle(el).transform;
3913
- if (!tr || tr === "none") return new DOMMatrix();
3914
- try {
3915
- return new DOMMatrix(tr);
3916
- } catch {
3917
- return new WebKitCSSMatrix(tr);
3918
- }
4062
+ function parseTransformOriginPx(cs, w, h) {
4063
+ const raw = (cs.transformOrigin || "0 0").trim().split(/\s+/);
4064
+ const [oxRaw, oyRaw] = [raw[0] || "0", raw[1] || "0"];
4065
+ const toPx = (token, size) => {
4066
+ const t = token.toLowerCase();
4067
+ if (t === "left" || t === "top") return 0;
4068
+ if (t === "center") return size / 2;
4069
+ if (t === "right") return size;
4070
+ if (t === "bottom") return size;
4071
+ if (t.endsWith("px")) return parseFloat(t) || 0;
4072
+ if (t.endsWith("%")) return (parseFloat(t) || 0) * size / 100;
4073
+ if (/^-?\d+(\.\d+)?$/.test(t)) return parseFloat(t) || 0;
4074
+ return 0;
4075
+ };
4076
+ return {
4077
+ ox: toPx(oxRaw, w),
4078
+ oy: toPx(oyRaw, h)
4079
+ };
3919
4080
  }
3920
4081
  function readIndividualTransforms(el) {
3921
4082
  const out = { rotate: "0deg", scale: null, translate: null };
@@ -3972,35 +4133,6 @@ function readIndividualTransforms(el) {
3972
4133
  out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
3973
4134
  return out;
3974
4135
  }
3975
- function hasBBoxAffectingTransform(el) {
3976
- const cs = getComputedStyle(el);
3977
- const t = cs.transform || "none";
3978
- const hasMatrix = t !== "none" && !/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,\s*0\s*,\s*0\s*\)$/i.test(t);
3979
- if (hasMatrix) return true;
3980
- const r = cs.rotate && cs.rotate !== "none" && cs.rotate !== "0deg";
3981
- const s = cs.scale && cs.scale !== "none" && cs.scale !== "1";
3982
- const tr = cs.translate && cs.translate !== "none" && cs.translate !== "0px 0px";
3983
- return Boolean(r || s || tr);
3984
- }
3985
- function parseTransformOriginPx(cs, w, h) {
3986
- const raw = (cs.transformOrigin || "0 0").trim().split(/\s+/);
3987
- const [oxRaw, oyRaw] = [raw[0] || "0", raw[1] || "0"];
3988
- const toPx = (token, size) => {
3989
- const t = token.toLowerCase();
3990
- if (t === "left" || t === "top") return 0;
3991
- if (t === "center") return size / 2;
3992
- if (t === "right") return size;
3993
- if (t === "bottom") return size;
3994
- if (t.endsWith("px")) return parseFloat(t) || 0;
3995
- if (t.endsWith("%")) return (parseFloat(t) || 0) * size / 100;
3996
- if (/^-?\d+(\.\d+)?$/.test(t)) return parseFloat(t) || 0;
3997
- return 0;
3998
- };
3999
- return {
4000
- ox: toPx(oxRaw, w),
4001
- oy: toPx(oyRaw, h)
4002
- };
4003
- }
4004
4136
  var __measureHost = null;
4005
4137
  function getMeasureHost() {
4006
4138
  if (__measureHost) return __measureHost;
@@ -4035,6 +4167,255 @@ function readTotalTransformMatrix(t) {
4035
4167
  host.removeChild(tmp);
4036
4168
  return M;
4037
4169
  }
4170
+ function hasBBoxAffectingTransform(el) {
4171
+ const cs = getComputedStyle(el);
4172
+ const t = cs.transform || "none";
4173
+ const hasMatrix = t !== "none" && !/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,\s*0\s*,\s*0\s*\)$/i.test(t);
4174
+ if (hasMatrix) return true;
4175
+ const r = cs.rotate && cs.rotate !== "none" && cs.rotate !== "0deg";
4176
+ const s = cs.scale && cs.scale !== "none" && cs.scale !== "1";
4177
+ const tr = cs.translate && cs.translate !== "none" && cs.translate !== "0px 0px";
4178
+ return Boolean(r || s || tr);
4179
+ }
4180
+ function matrixFromComputed(el) {
4181
+ const tr = getComputedStyle(el).transform;
4182
+ if (!tr || tr === "none") return new DOMMatrix();
4183
+ try {
4184
+ return new DOMMatrix(tr);
4185
+ } catch {
4186
+ return new WebKitCSSMatrix(tr);
4187
+ }
4188
+ }
4189
+
4190
+ // src/core/capture.js
4191
+ async function captureDOM(element, options) {
4192
+ if (!element) throw new Error("Element cannot be null or undefined");
4193
+ applyCachePolicy(options.cache);
4194
+ const fast = options.fast;
4195
+ const outerTransforms = options.outerTransforms !== false;
4196
+ const outerShadows = !!options.outerShadows;
4197
+ let state = { element, options, plugins: options.plugins };
4198
+ let clone, classCSS, styleCache;
4199
+ let fontsCSS = "";
4200
+ let baseCSS = "";
4201
+ let dataURL;
4202
+ let svgString;
4203
+ let rootTransform2D = null;
4204
+ await runHook("beforeSnap", state);
4205
+ await runHook("beforeClone", state);
4206
+ const undoClamp = lineClamp(state.element);
4207
+ try {
4208
+ ({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
4209
+ if (!outerTransforms && clone) {
4210
+ rootTransform2D = normalizeRootTransforms(state.element, clone);
4211
+ }
4212
+ if (!outerShadows && clone) {
4213
+ stripRootShadows(state.element, clone);
4214
+ }
4215
+ } finally {
4216
+ undoClamp();
4217
+ }
4218
+ state = { clone, classCSS, styleCache, ...state };
4219
+ await runHook("afterClone", state);
4220
+ sanitizeCloneForXHTML(state.clone);
4221
+ if (state.options?.excludeMode === "remove") {
4222
+ try {
4223
+ shrinkAutoSizeBoxes(state.element, state.clone, state.styleCache);
4224
+ } catch (e) {
4225
+ console.warn("[snapdom] shrink pass failed:", e);
4226
+ }
4227
+ }
4228
+ try {
4229
+ await ligatureIconToImage(state.clone, state.element);
4230
+ } catch {
4231
+ }
4232
+ await new Promise((resolve) => {
4233
+ idle(async () => {
4234
+ await inlineImages(state.clone, state.options);
4235
+ resolve();
4236
+ }, { fast });
4237
+ });
4238
+ await new Promise((resolve) => {
4239
+ idle(async () => {
4240
+ await inlineBackgroundImages(state.element, state.clone, state.styleCache, state.options);
4241
+ resolve();
4242
+ }, { fast });
4243
+ });
4244
+ if (options.embedFonts) {
4245
+ await new Promise((resolve) => {
4246
+ idle(async () => {
4247
+ const required = collectUsedFontVariants(state.element);
4248
+ const usedCodepoints = collectUsedCodepoints(state.element);
4249
+ if (isSafari()) {
4250
+ const families = new Set(
4251
+ Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
4252
+ );
4253
+ await ensureFontsReady(families, 1);
4254
+ }
4255
+ fontsCSS = await embedCustomFonts({
4256
+ required,
4257
+ usedCodepoints,
4258
+ preCached: false,
4259
+ exclude: state.options.excludeFonts,
4260
+ useProxy: state.options.useProxy
4261
+ });
4262
+ resolve();
4263
+ }, { fast });
4264
+ });
4265
+ }
4266
+ const usedTags = collectUsedTagNames(state.clone).sort();
4267
+ const tagKey = usedTags.join(",");
4268
+ if (cache.baseStyle.has(tagKey)) {
4269
+ baseCSS = cache.baseStyle.get(tagKey);
4270
+ } else {
4271
+ await new Promise((resolve) => {
4272
+ idle(() => {
4273
+ baseCSS = generateDedupedBaseCSS(usedTags);
4274
+ cache.baseStyle.set(tagKey, baseCSS);
4275
+ resolve();
4276
+ }, { fast });
4277
+ });
4278
+ }
4279
+ state = { fontsCSS, baseCSS, ...state };
4280
+ await runHook("beforeRender", state);
4281
+ await new Promise((resolve) => {
4282
+ idle(() => {
4283
+ const csEl = getComputedStyle(state.element);
4284
+ const rect = state.element.getBoundingClientRect();
4285
+ let w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
4286
+ let h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
4287
+ if (state.options?.excludeMode === "remove") {
4288
+ const hEst = estimateKeptHeight(state.element, state.options);
4289
+ const EPS = 1;
4290
+ if (Number.isFinite(hEst) && hEst > 0) {
4291
+ h0 = Math.max(1, Math.min(h0, limitDecimals(hEst + EPS)));
4292
+ }
4293
+ }
4294
+ const coerceNum = (v, def = NaN) => {
4295
+ const n = typeof v === "string" ? parseFloat(v) : v;
4296
+ return Number.isFinite(n) ? n : def;
4297
+ };
4298
+ const optW = coerceNum(state.options.width);
4299
+ const optH = coerceNum(state.options.height);
4300
+ let w = w0, h = h0;
4301
+ const hasW = Number.isFinite(optW);
4302
+ const hasH = Number.isFinite(optH);
4303
+ const aspect0 = h0 > 0 ? w0 / h0 : 1;
4304
+ if (hasW && hasH) {
4305
+ w = Math.max(1, limitDecimals(optW));
4306
+ h = Math.max(1, limitDecimals(optH));
4307
+ } else if (hasW) {
4308
+ w = Math.max(1, limitDecimals(optW));
4309
+ h = Math.max(1, limitDecimals(w / (aspect0 || 1)));
4310
+ } else if (hasH) {
4311
+ h = Math.max(1, limitDecimals(optH));
4312
+ w = Math.max(1, limitDecimals(h * (aspect0 || 1)));
4313
+ } else {
4314
+ w = w0;
4315
+ h = h0;
4316
+ }
4317
+ let minX = 0, minY = 0, maxX = w0, maxY = h0;
4318
+ if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
4319
+ const M2 = {
4320
+ a: rootTransform2D.a,
4321
+ b: rootTransform2D.b || 0,
4322
+ c: rootTransform2D.c || 0,
4323
+ d: rootTransform2D.d || 1,
4324
+ e: 0,
4325
+ f: 0
4326
+ };
4327
+ const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
4328
+ minX = limitDecimals(bb2.minX);
4329
+ minY = limitDecimals(bb2.minY);
4330
+ maxX = limitDecimals(bb2.maxX);
4331
+ maxY = limitDecimals(bb2.maxY);
4332
+ } else {
4333
+ const useTFBBox = outerTransforms && hasTFBBox(state.element);
4334
+ if (useTFBBox) {
4335
+ const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
4336
+ const ind2 = readIndividualTransforms(state.element);
4337
+ const TOTAL = readTotalTransformMatrix({
4338
+ baseTransform: baseTransform2,
4339
+ rotate: ind2.rotate || "0deg",
4340
+ scale: ind2.scale,
4341
+ translate: ind2.translate
4342
+ });
4343
+ const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
4344
+ const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
4345
+ const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
4346
+ minX = limitDecimals(bb.minX);
4347
+ minY = limitDecimals(bb.minY);
4348
+ maxX = limitDecimals(bb.maxX);
4349
+ maxY = limitDecimals(bb.maxY);
4350
+ }
4351
+ }
4352
+ const bleedShadow = parseBoxShadow(csEl);
4353
+ const bleedBlur = parseFilterBlur(csEl);
4354
+ const bleedOutline = parseOutline(csEl);
4355
+ const drop = parseFilterDropShadows(csEl);
4356
+ const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
4357
+ top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
4358
+ right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
4359
+ bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
4360
+ left: limitDecimals(bleedShadow.left + bleedBlur.left + bleedOutline.left + drop.bleed.left)
4361
+ };
4362
+ minX = limitDecimals(minX - bleed.left);
4363
+ minY = limitDecimals(minY - bleed.top);
4364
+ maxX = limitDecimals(maxX + bleed.right);
4365
+ maxY = limitDecimals(maxY + bleed.bottom);
4366
+ const vbW0 = Math.max(1, limitDecimals(maxX - minX));
4367
+ const vbH0 = Math.max(1, limitDecimals(maxY - minY));
4368
+ const scaleW = hasW || hasH ? limitDecimals(w / w0) : 1;
4369
+ const scaleH = hasH || hasW ? limitDecimals(h / h0) : 1;
4370
+ const outW = Math.max(1, limitDecimals(vbW0 * scaleW));
4371
+ const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
4372
+ const svgNS = "http://www.w3.org/2000/svg";
4373
+ const basePad = isSafari() ? 1 : 0;
4374
+ const extraPad = !outerTransforms ? 1 : 0;
4375
+ const pad = limitDecimals(basePad + extraPad);
4376
+ const fo = document.createElementNS(svgNS, "foreignObject");
4377
+ const vbMinX = limitDecimals(minX);
4378
+ const vbMinY = limitDecimals(minY);
4379
+ fo.setAttribute("x", String(limitDecimals(-(vbMinX - pad))));
4380
+ fo.setAttribute("y", String(limitDecimals(-(vbMinY - pad))));
4381
+ fo.setAttribute("width", String(limitDecimals(w0 + pad * 2)));
4382
+ fo.setAttribute("height", String(limitDecimals(h0 + pad * 2)));
4383
+ fo.style.overflow = "visible";
4384
+ const styleTag = document.createElement("style");
4385
+ styleTag.textContent = state.baseCSS + state.fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + state.classCSS;
4386
+ fo.appendChild(styleTag);
4387
+ const container = document.createElement("div");
4388
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
4389
+ container.style.width = `${limitDecimals(w0)}px`;
4390
+ container.style.height = `${limitDecimals(h0)}px`;
4391
+ container.style.overflow = "visible";
4392
+ state.clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
4393
+ container.appendChild(state.clone);
4394
+ fo.appendChild(container);
4395
+ const serializer = new XMLSerializer();
4396
+ const foString = serializer.serializeToString(fo);
4397
+ const vbW = limitDecimals(vbW0 + pad * 2);
4398
+ const vbH = limitDecimals(vbH0 + pad * 2);
4399
+ const wantsSize = hasW || hasH;
4400
+ options.meta = { w0, h0, vbW, vbH, targetW: w, targetH: h };
4401
+ const svgOutW = isSafari() && wantsSize ? vbW : limitDecimals(outW + pad * 2);
4402
+ const svgOutH = isSafari() && wantsSize ? vbH : limitDecimals(outH + pad * 2);
4403
+ const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
4404
+ const svgFooter = "</svg>";
4405
+ svgString = svgHeader + foString + svgFooter;
4406
+ dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
4407
+ state = { svgString, dataURL, ...state };
4408
+ resolve();
4409
+ }, { fast });
4410
+ });
4411
+ await runHook("afterRender", state);
4412
+ const sandbox = document.getElementById("snapdom-sandbox");
4413
+ if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
4414
+ return state.dataURL;
4415
+ }
4416
+ function hasTFBBox(el) {
4417
+ return hasBBoxAffectingTransform(el);
4418
+ }
4038
4419
 
4039
4420
  // src/core/context.js
4040
4421
  function normalizeCachePolicy(v) {
@@ -4191,7 +4572,7 @@ function maybeConvertBoxShadowForSafari(url) {
4191
4572
  }
4192
4573
  }
4193
4574
  async function toCanvas(url, options) {
4194
- let { width: optW, height: optH, scale = 1, dpr = 1, meta = {} } = options;
4575
+ let { width: optW, height: optH, scale = 1, dpr = 1, meta = {}, backgroundColor } = options;
4195
4576
  url = maybeConvertBoxShadowForSafari(url);
4196
4577
  const img = new Image();
4197
4578
  img.loading = "eager";
@@ -4230,6 +4611,12 @@ async function toCanvas(url, options) {
4230
4611
  canvas.style.height = `${outH}px`;
4231
4612
  const ctx = canvas.getContext("2d");
4232
4613
  if (dpr !== 1) ctx.scale(dpr, dpr);
4614
+ if (backgroundColor) {
4615
+ ctx.save();
4616
+ ctx.fillStyle = backgroundColor;
4617
+ ctx.fillRect(0, 0, outW, outH);
4618
+ ctx.restore();
4619
+ }
4233
4620
  ctx.drawImage(img, 0, 0, outW, outH);
4234
4621
  return canvas;
4235
4622
  }
@@ -4237,12 +4624,11 @@ async function toCanvas(url, options) {
4237
4624
  // src/modules/rasterize.js
4238
4625
  async function rasterize(url, options) {
4239
4626
  const canvas = await toCanvas(url, options);
4240
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4241
4627
  const img = new Image();
4242
- img.src = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4628
+ img.src = canvas.toDataURL(`image/${options.format}`, options.quality);
4243
4629
  await img.decode();
4244
- img.style.width = `${finalCanvas.width / options.dpr}px`;
4245
- img.style.height = `${finalCanvas.height / options.dpr}px`;
4630
+ img.style.width = `${canvas.width / options.dpr}px`;
4631
+ img.style.height = `${canvas.height / options.dpr}px`;
4246
4632
  return img;
4247
4633
  }
4248
4634
 
@@ -4302,13 +4688,8 @@ async function toBlob(url, options) {
4302
4688
  return new Blob([svgText], { type: "image/svg+xml" });
4303
4689
  }
4304
4690
  const canvas = await toCanvas(url, options);
4305
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4306
4691
  return new Promise(
4307
- (resolve) => finalCanvas.toBlob(
4308
- (blob) => resolve(blob),
4309
- `image/${type}`,
4310
- options.quality
4311
- )
4692
+ (resolve) => canvas.toBlob((blob) => resolve(blob), `image/${type}`, options.quality)
4312
4693
  );
4313
4694
  }
4314
4695
 
@@ -4326,9 +4707,8 @@ async function download(url, options) {
4326
4707
  return;
4327
4708
  }
4328
4709
  const canvas = await toCanvas(url, options);
4329
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4330
4710
  const a = document.createElement("a");
4331
- a.href = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4711
+ a.href = canvas.toDataURL(`image/${options.format}`, options.quality);
4332
4712
  a.download = options.filename;
4333
4713
  a.click();
4334
4714
  }