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

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.2
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",
@@ -1421,17 +1433,19 @@ var defaultIconFonts = [
1421
1433
  /layui/i,
1422
1434
  /lucide/i
1423
1435
  ];
1436
+ var ICON_FONT_URLS = Object.assign({
1437
+ materialIconsFilled: "https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2",
1438
+ materialIconsOutlined: "https://fonts.gstatic.com/s/materialiconsoutlined/v110/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUcel5euIg.woff2",
1439
+ materialIconsRound: "https://fonts.gstatic.com/s/materialiconsround/v109/LDItaoyNOAY6Uewc665JcIzCKsKc_M9flwmPq_HTTw.woff2",
1440
+ materialIconsSharp: "https://fonts.gstatic.com/s/materialiconssharp/v110/oPWQ_lt5nv4pWNJpghLP75WiFR4kLh3kvmvRImcycg.woff2"
1441
+ }, typeof window !== "undefined" && window.__SNAPDOM_ICON_FONTS__ || {});
1424
1442
  var userIconFonts = [];
1425
1443
  function extendIconFonts(fonts) {
1426
1444
  const list = Array.isArray(fonts) ? fonts : [fonts];
1427
1445
  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
- }
1446
+ if (f instanceof RegExp) userIconFonts.push(f);
1447
+ else if (typeof f === "string") userIconFonts.push(new RegExp(f, "i"));
1448
+ else console.warn("[snapdom] Ignored invalid iconFont value:", f);
1435
1449
  }
1436
1450
  }
1437
1451
  function isIconFont2(input) {
@@ -1447,10 +1461,62 @@ function isMaterialFamily(family = "") {
1447
1461
  const s = String(family).toLowerCase();
1448
1462
  return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
1449
1463
  }
1464
+ var loadedCanvasFamilies = /* @__PURE__ */ new Map();
1465
+ function parseAxes(variation = "") {
1466
+ const out = /* @__PURE__ */ Object.create(null);
1467
+ const v = String(variation || "");
1468
+ const rx = /['"]?\s*([A-Za-z]{3,4})\s*['"]?\s*([+-]?\d+(?:\.\d+)?)\s*/g;
1469
+ let m;
1470
+ while (m = rx.exec(v)) out[m[1].toUpperCase()] = Number(m[2]);
1471
+ return out;
1472
+ }
1473
+ async function ensureLigatureCanvasFont(cssFamily, className, axes) {
1474
+ const fam = String(cssFamily || "");
1475
+ const lowerFam = fam.toLowerCase();
1476
+ const cls = String(className || "").toLowerCase();
1477
+ if (/\bmaterial\s*icons\b/.test(lowerFam) && !/\bsymbols\b/.test(lowerFam)) {
1478
+ return { familyForMeasure: fam, familyForCanvas: fam };
1479
+ }
1480
+ const isSymbols = /\bmaterial\s*symbols\b/.test(lowerFam);
1481
+ if (!isSymbols) {
1482
+ return { familyForMeasure: fam, familyForCanvas: fam };
1483
+ }
1484
+ const FILL = axes && (axes.FILL ?? axes.fill);
1485
+ let style = "outlined";
1486
+ if (/\brounded\b/.test(cls) || /\bround\b/.test(cls)) style = "rounded";
1487
+ else if (/\bsharp\b/.test(cls)) style = "sharp";
1488
+ else if (/\boutlined\b/.test(cls)) style = "outlined";
1489
+ const filled = FILL === 1;
1490
+ let pick = null;
1491
+ if (filled) {
1492
+ if (style === "outlined" && ICON_FONT_URLS.materialIconsFilled) {
1493
+ pick = { url: ICON_FONT_URLS.materialIconsFilled, alias: "snapdom-mi-filled" };
1494
+ } else if (style === "rounded" && ICON_FONT_URLS.materialIconsRound) {
1495
+ pick = { url: ICON_FONT_URLS.materialIconsRound, alias: "snapdom-mi-round" };
1496
+ } else if (style === "sharp" && ICON_FONT_URLS.materialIconsSharp) {
1497
+ pick = { url: ICON_FONT_URLS.materialIconsSharp, alias: "snapdom-mi-sharp" };
1498
+ }
1499
+ }
1500
+ if (!pick) {
1501
+ return { familyForMeasure: fam, familyForCanvas: fam };
1502
+ }
1503
+ if (!loadedCanvasFamilies.has(pick.alias)) {
1504
+ try {
1505
+ const ff = new FontFace(pick.alias, `url(${pick.url})`, { style: "normal", weight: "400" });
1506
+ document.fonts.add(ff);
1507
+ await ff.load();
1508
+ loadedCanvasFamilies.set(pick.alias, true);
1509
+ } catch {
1510
+ return { familyForMeasure: fam, familyForCanvas: fam };
1511
+ }
1512
+ }
1513
+ const quoted = `"${pick.alias}"`;
1514
+ return { familyForMeasure: quoted, familyForCanvas: quoted };
1515
+ }
1450
1516
  async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1451
1517
  try {
1452
1518
  await Promise.all([
1453
- document.fonts.load(`400 ${px}px "${family.replace(/["']/g, "")}"`),
1519
+ document.fonts.load(`400 ${px}px "${String(family).replace(/["']/g, "")}"`),
1454
1520
  document.fonts.ready
1455
1521
  ]);
1456
1522
  } catch {
@@ -1458,10 +1524,8 @@ async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1458
1524
  }
1459
1525
  function resolvePaintColor(cs) {
1460
1526
  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
- }
1527
+ const isTransparent = /^transparent$/i.test(fill) || /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(fill);
1528
+ if (fill && !isTransparent && fill.toLowerCase() !== "currentcolor") return fill;
1465
1529
  const c = cs.color?.trim();
1466
1530
  return c && c !== "inherit" ? c : "#000";
1467
1531
  }
@@ -1470,18 +1534,21 @@ async function materialIconToImage(ligatureText, {
1470
1534
  weight = "normal",
1471
1535
  fontSize = 32,
1472
1536
  color = "#000",
1473
- variation = ""
1537
+ variation = "",
1538
+ className = ""
1474
1539
  } = {}) {
1475
1540
  const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
1476
1541
  const dpr = window.devicePixelRatio || 1;
1477
- await ensureMaterialFontsReady(fam, fontSize);
1542
+ const axes = parseAxes(variation);
1543
+ const { familyForMeasure, familyForCanvas } = await ensureLigatureCanvasFont(fam, className, axes);
1544
+ await ensureMaterialFontsReady(familyForCanvas.replace(/^["']+|["']+$/g, ""), fontSize);
1478
1545
  const span = document.createElement("span");
1479
1546
  span.textContent = ligatureText;
1480
1547
  span.style.position = "absolute";
1481
1548
  span.style.visibility = "hidden";
1482
1549
  span.style.left = "-99999px";
1483
1550
  span.style.whiteSpace = "nowrap";
1484
- span.style.fontFamily = `"${fam}"`;
1551
+ span.style.fontFamily = familyForMeasure;
1485
1552
  span.style.fontWeight = String(weight || "normal");
1486
1553
  span.style.fontSize = `${fontSize}px`;
1487
1554
  span.style.lineHeight = "1";
@@ -1489,7 +1556,6 @@ async function materialIconToImage(ligatureText, {
1489
1556
  span.style.padding = "0";
1490
1557
  span.style.fontFeatureSettings = "'liga' 1";
1491
1558
  span.style.fontVariantLigatures = "normal";
1492
- if (variation) span.style.fontVariationSettings = variation;
1493
1559
  span.style.color = color;
1494
1560
  document.body.appendChild(span);
1495
1561
  const rect = span.getBoundingClientRect();
@@ -1501,7 +1567,7 @@ async function materialIconToImage(ligatureText, {
1501
1567
  canvas.height = height * dpr;
1502
1568
  const ctx = canvas.getContext("2d");
1503
1569
  ctx.scale(dpr, dpr);
1504
- ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px "${fam}"`;
1570
+ ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px ${familyForCanvas}`;
1505
1571
  ctx.textAlign = "left";
1506
1572
  ctx.textBaseline = "top";
1507
1573
  ctx.fillStyle = color;
@@ -1529,21 +1595,23 @@ async function ligatureIconToImage(cloneRoot, sourceRoot) {
1529
1595
  const el = cloneNodes[i];
1530
1596
  const src = sourceNodes[i] || null;
1531
1597
  try {
1532
- const csSrc = src ? getComputedStyle(src) : getComputedStyle(el);
1533
- const family = csSrc.fontFamily || "Material Icons";
1598
+ const cs = src ? getComputedStyle(src) : getComputedStyle(el);
1599
+ const family = cs.fontFamily || "Material Icons";
1534
1600
  if (!isMaterialFamily(family)) continue;
1535
1601
  const text = (src || el).textContent.trim();
1536
1602
  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 : "";
1603
+ const size = parseInt(cs.fontSize, 10) || 24;
1604
+ const weight = cs.fontWeight && cs.fontWeight !== "normal" ? cs.fontWeight : "normal";
1605
+ const color = resolvePaintColor(cs);
1606
+ const variation = cs.fontVariationSettings && cs.fontVariationSettings !== "normal" ? cs.fontVariationSettings : "";
1607
+ const className = (src || el).className || "";
1541
1608
  const { dataUrl, width, height } = await materialIconToImage(text, {
1542
1609
  family,
1543
1610
  weight,
1544
1611
  fontSize: size,
1545
1612
  color,
1546
- variation
1613
+ variation,
1614
+ className
1547
1615
  });
1548
1616
  el.textContent = "";
1549
1617
  const img = el.ownerDocument.createElement("img");
@@ -1909,7 +1977,7 @@ function dedupeFontFaces(cssText) {
1909
1977
  const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1910
1978
  const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1911
1979
  const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1912
- const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
1980
+ const srcRaw = (block.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
1913
1981
  const urls = extractSrcUrls(srcRaw, location.href);
1914
1982
  const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
1915
1983
  const key = [
@@ -2117,7 +2185,7 @@ async function embedCustomFonts({
2117
2185
  const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
2118
2186
  const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
2119
2187
  const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
2120
- const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
2188
+ const srcRaw = (face.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
2121
2189
  const srcUrls = extractSrcUrls(srcRaw, link.href);
2122
2190
  if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
2123
2191
  const ranges = parseUnicodeRange(urange);
@@ -3559,6 +3627,50 @@ function stripRootShadows(originalEl, cloneRoot) {
3559
3627
  } catch {
3560
3628
  }
3561
3629
  }
3630
+ function removeAllComments(root) {
3631
+ const it = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
3632
+ const toRemove = [];
3633
+ while (it.nextNode()) toRemove.push(it.currentNode);
3634
+ for (const n of toRemove) n.remove();
3635
+ }
3636
+ function sanitizeAttributesForXHTML(root, opts = {}) {
3637
+ const { stripFrameworkDirectives = true } = opts;
3638
+ const ALLOWED_PREFIXES = /* @__PURE__ */ new Set(["xml", "xlink"]);
3639
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
3640
+ while (walker.nextNode()) {
3641
+ const el = walker.currentNode;
3642
+ for (const attr of Array.from(el.attributes)) {
3643
+ const name = attr.name;
3644
+ if (name.includes("@")) {
3645
+ el.removeAttribute(name);
3646
+ continue;
3647
+ }
3648
+ if (name.includes(":")) {
3649
+ const prefix = name.split(":", 1)[0];
3650
+ if (!ALLOWED_PREFIXES.has(prefix)) {
3651
+ el.removeAttribute(name);
3652
+ continue;
3653
+ }
3654
+ }
3655
+ if (!stripFrameworkDirectives) continue;
3656
+ if (name.startsWith("x-") || // Alpine
3657
+ name.startsWith("v-") || // Vue
3658
+ name.startsWith(":") || // Vue/Alpine shorthand
3659
+ name.startsWith("on:") || // Svelte
3660
+ name.startsWith("bind:") || // Svelte
3661
+ name.startsWith("let:") || // Svelte
3662
+ name.startsWith("class:")) {
3663
+ el.removeAttribute(name);
3664
+ continue;
3665
+ }
3666
+ }
3667
+ }
3668
+ }
3669
+ function sanitizeCloneForXHTML(root, opts = {}) {
3670
+ if (!root) return;
3671
+ sanitizeAttributesForXHTML(root, opts);
3672
+ removeAllComments(root);
3673
+ }
3562
3674
  async function captureDOM(element, options) {
3563
3675
  if (!element) throw new Error("Element cannot be null or undefined");
3564
3676
  applyCachePolicy(options.cache);
@@ -3588,6 +3700,14 @@ async function captureDOM(element, options) {
3588
3700
  }
3589
3701
  state = { clone, classCSS, styleCache, ...state };
3590
3702
  await runHook("afterClone", state);
3703
+ sanitizeCloneForXHTML(state.clone);
3704
+ if (state.options?.excludeMode === "remove") {
3705
+ try {
3706
+ shrinkAutoSizeBoxes(state.element, state.clone, state.styleCache);
3707
+ } catch (e) {
3708
+ console.warn("[snapdom] shrink pass failed:", e);
3709
+ }
3710
+ }
3591
3711
  try {
3592
3712
  await ligatureIconToImage(state.clone, state.element);
3593
3713
  } catch {
@@ -3645,8 +3765,15 @@ async function captureDOM(element, options) {
3645
3765
  idle(() => {
3646
3766
  const csEl = getComputedStyle(state.element);
3647
3767
  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));
3768
+ let w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3769
+ let h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
3770
+ if (state.options?.excludeMode === "remove") {
3771
+ const hEst = estimateKeptHeight(state.element, state.options);
3772
+ const EPS = 1;
3773
+ if (Number.isFinite(hEst) && hEst > 0) {
3774
+ h0 = Math.max(1, Math.min(h0, limitDecimals(hEst + EPS)));
3775
+ }
3776
+ }
3650
3777
  const coerceNum = (v, def = NaN) => {
3651
3778
  const n = typeof v === "string" ? parseFloat(v) : v;
3652
3779
  return Number.isFinite(n) ? n : def;
@@ -3769,6 +3896,57 @@ async function captureDOM(element, options) {
3769
3896
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3770
3897
  return state.dataURL;
3771
3898
  }
3899
+ function authorHasExplicitSize(el) {
3900
+ try {
3901
+ const s = el.getAttribute?.("style") || "";
3902
+ return /\b(height|width|block-size|inline-size)\s*:/.test(s);
3903
+ } catch {
3904
+ return false;
3905
+ }
3906
+ }
3907
+ function isReplacedElement(el) {
3908
+ return el instanceof HTMLImageElement || el instanceof HTMLCanvasElement || el instanceof HTMLVideoElement || el instanceof HTMLIFrameElement || el instanceof SVGElement || el instanceof HTMLObjectElement || el instanceof HTMLEmbedElement;
3909
+ }
3910
+ function shouldShrinkBox(srcEl, cs) {
3911
+ if (!(srcEl instanceof Element)) return false;
3912
+ if (authorHasExplicitSize(srcEl)) return false;
3913
+ if (isReplacedElement(srcEl)) return false;
3914
+ const pos = cs.position;
3915
+ if (pos === "absolute" || pos === "fixed" || pos === "sticky") return false;
3916
+ const disp = cs.display || "";
3917
+ if (disp.includes("flex") || disp.includes("grid") || disp.startsWith("table")) return false;
3918
+ if (cs.transform && cs.transform !== "none") return false;
3919
+ return true;
3920
+ }
3921
+ function shrinkAutoSizeBoxes(sourceRoot, cloneRoot, styleCache = /* @__PURE__ */ new Map()) {
3922
+ function walk(src, cln) {
3923
+ if (!(src instanceof Element) || !(cln instanceof Element)) return;
3924
+ const lostKids = src.childElementCount > cln.childElementCount;
3925
+ const cs = styleCache.get(src) || getComputedStyle(src);
3926
+ if (!styleCache.has(src)) styleCache.set(src, cs);
3927
+ if (lostKids && shouldShrinkBox(src, cs)) {
3928
+ if (!cln.style.height) cln.style.height = "auto";
3929
+ if (!cln.style.width) cln.style.width = "auto";
3930
+ cln.style.removeProperty("block-size");
3931
+ cln.style.removeProperty("inline-size");
3932
+ if (!cln.style.minHeight) cln.style.minHeight = "0";
3933
+ if (!cln.style.minWidth) cln.style.minWidth = "0";
3934
+ if (!cln.style.maxHeight) cln.style.maxHeight = "none";
3935
+ if (!cln.style.maxWidth) cln.style.maxWidth = "none";
3936
+ const oy = cs.overflowY || cs.overflowBlock || "visible";
3937
+ const ox = cs.overflowX || cs.overflowInline || "visible";
3938
+ if (oy !== "visible" || ox !== "visible") {
3939
+ cln.style.overflow = "visible";
3940
+ }
3941
+ }
3942
+ const sKids = Array.from(src.children);
3943
+ const cKids = Array.from(cln.children);
3944
+ for (let i = 0; i < Math.min(sKids.length, cKids.length); i++) {
3945
+ walk(sKids[i], cKids[i]);
3946
+ }
3947
+ }
3948
+ walk(sourceRoot, cloneRoot);
3949
+ }
3772
3950
  var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
3773
3951
  function parseFilterDropShadows(cs) {
3774
3952
  const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
@@ -4035,6 +4213,52 @@ function readTotalTransformMatrix(t) {
4035
4213
  host.removeChild(tmp);
4036
4214
  return M;
4037
4215
  }
4216
+ function isInNormalFlow(el) {
4217
+ const cs = getComputedStyle(el);
4218
+ if (cs.display === "none") return false;
4219
+ if (cs.position === "absolute" || cs.position === "fixed" || cs.position === "sticky") return false;
4220
+ if ((cs.cssFloat || cs.float || "none") !== "none") return false;
4221
+ if (cs.transform && cs.transform !== "none") return false;
4222
+ return true;
4223
+ }
4224
+ function willBeExcluded(el, options) {
4225
+ if (!(el instanceof Element)) return false;
4226
+ if (el.getAttribute("data-capture") === "exclude" && options?.excludeMode === "remove") return true;
4227
+ if (Array.isArray(options?.exclude)) {
4228
+ for (const sel of options.exclude) {
4229
+ try {
4230
+ if (el.matches(sel)) return options.excludeMode === "remove";
4231
+ } catch {
4232
+ }
4233
+ }
4234
+ }
4235
+ return false;
4236
+ }
4237
+ function estimateKeptHeight(container, options) {
4238
+ const csC = getComputedStyle(container);
4239
+ const rC = container.getBoundingClientRect();
4240
+ let minTop = Infinity;
4241
+ let maxBottom = -Infinity;
4242
+ let found = false;
4243
+ const kids = Array.from(container.children);
4244
+ for (const k of kids) {
4245
+ if (willBeExcluded(k, options)) continue;
4246
+ if (!isInNormalFlow(k)) continue;
4247
+ const rk = k.getBoundingClientRect();
4248
+ const top = rk.top - rC.top;
4249
+ const bottom = rk.bottom - rC.top;
4250
+ if (bottom <= top) continue;
4251
+ if (top < minTop) minTop = top;
4252
+ if (bottom > maxBottom) maxBottom = bottom;
4253
+ found = true;
4254
+ }
4255
+ const contentSpan = found ? Math.max(0, maxBottom - minTop) : 0;
4256
+ const bt = parseFloat(csC.borderTopWidth) || 0;
4257
+ const bb = parseFloat(csC.borderBottomWidth) || 0;
4258
+ const pt = parseFloat(csC.paddingTop) || 0;
4259
+ const pb = parseFloat(csC.paddingBottom) || 0;
4260
+ return bt + bb + pt + pb + contentSpan;
4261
+ }
4038
4262
 
4039
4263
  // src/core/context.js
4040
4264
  function normalizeCachePolicy(v) {
@@ -4191,7 +4415,7 @@ function maybeConvertBoxShadowForSafari(url) {
4191
4415
  }
4192
4416
  }
4193
4417
  async function toCanvas(url, options) {
4194
- let { width: optW, height: optH, scale = 1, dpr = 1, meta = {} } = options;
4418
+ let { width: optW, height: optH, scale = 1, dpr = 1, meta = {}, backgroundColor } = options;
4195
4419
  url = maybeConvertBoxShadowForSafari(url);
4196
4420
  const img = new Image();
4197
4421
  img.loading = "eager";
@@ -4230,6 +4454,12 @@ async function toCanvas(url, options) {
4230
4454
  canvas.style.height = `${outH}px`;
4231
4455
  const ctx = canvas.getContext("2d");
4232
4456
  if (dpr !== 1) ctx.scale(dpr, dpr);
4457
+ if (backgroundColor) {
4458
+ ctx.save();
4459
+ ctx.fillStyle = backgroundColor;
4460
+ ctx.fillRect(0, 0, outW, outH);
4461
+ ctx.restore();
4462
+ }
4233
4463
  ctx.drawImage(img, 0, 0, outW, outH);
4234
4464
  return canvas;
4235
4465
  }
@@ -4237,12 +4467,11 @@ async function toCanvas(url, options) {
4237
4467
  // src/modules/rasterize.js
4238
4468
  async function rasterize(url, options) {
4239
4469
  const canvas = await toCanvas(url, options);
4240
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4241
4470
  const img = new Image();
4242
- img.src = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4471
+ img.src = canvas.toDataURL(`image/${options.format}`, options.quality);
4243
4472
  await img.decode();
4244
- img.style.width = `${finalCanvas.width / options.dpr}px`;
4245
- img.style.height = `${finalCanvas.height / options.dpr}px`;
4473
+ img.style.width = `${canvas.width / options.dpr}px`;
4474
+ img.style.height = `${canvas.height / options.dpr}px`;
4246
4475
  return img;
4247
4476
  }
4248
4477
 
@@ -4302,13 +4531,8 @@ async function toBlob(url, options) {
4302
4531
  return new Blob([svgText], { type: "image/svg+xml" });
4303
4532
  }
4304
4533
  const canvas = await toCanvas(url, options);
4305
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4306
4534
  return new Promise(
4307
- (resolve) => finalCanvas.toBlob(
4308
- (blob) => resolve(blob),
4309
- `image/${type}`,
4310
- options.quality
4311
- )
4535
+ (resolve) => canvas.toBlob((blob) => resolve(blob), `image/${type}`, options.quality)
4312
4536
  );
4313
4537
  }
4314
4538
 
@@ -4326,9 +4550,8 @@ async function download(url, options) {
4326
4550
  return;
4327
4551
  }
4328
4552
  const canvas = await toCanvas(url, options);
4329
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4330
4553
  const a = document.createElement("a");
4331
- a.href = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4554
+ a.href = canvas.toDataURL(`image/${options.format}`, options.quality);
4332
4555
  a.download = options.filename;
4333
4556
  a.click();
4334
4557
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "2.0.0-dev.1",
3
+ "version": "2.0.0-dev.2",
4
4
  "description": "snapDOM captures HTML elements to images with exceptional speed and accuracy.",
5
5
  "main": "dist/snapdom.js",
6
6
  "module": "dist/snapdom.mjs",