@zumer/snapdom 2.0.0-dev.0 → 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.0
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) {
@@ -1443,6 +1457,177 @@ function isIconFont2(input) {
1443
1457
  if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
1444
1458
  return false;
1445
1459
  }
1460
+ function isMaterialFamily(family = "") {
1461
+ const s = String(family).toLowerCase();
1462
+ return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
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
+ }
1516
+ async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1517
+ try {
1518
+ await Promise.all([
1519
+ document.fonts.load(`400 ${px}px "${String(family).replace(/["']/g, "")}"`),
1520
+ document.fonts.ready
1521
+ ]);
1522
+ } catch {
1523
+ }
1524
+ }
1525
+ function resolvePaintColor(cs) {
1526
+ let fill = cs.getPropertyValue("-webkit-text-fill-color")?.trim() || "";
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;
1529
+ const c = cs.color?.trim();
1530
+ return c && c !== "inherit" ? c : "#000";
1531
+ }
1532
+ async function materialIconToImage(ligatureText, {
1533
+ family = "Material Icons",
1534
+ weight = "normal",
1535
+ fontSize = 32,
1536
+ color = "#000",
1537
+ variation = "",
1538
+ className = ""
1539
+ } = {}) {
1540
+ const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
1541
+ const dpr = window.devicePixelRatio || 1;
1542
+ const axes = parseAxes(variation);
1543
+ const { familyForMeasure, familyForCanvas } = await ensureLigatureCanvasFont(fam, className, axes);
1544
+ await ensureMaterialFontsReady(familyForCanvas.replace(/^["']+|["']+$/g, ""), fontSize);
1545
+ const span = document.createElement("span");
1546
+ span.textContent = ligatureText;
1547
+ span.style.position = "absolute";
1548
+ span.style.visibility = "hidden";
1549
+ span.style.left = "-99999px";
1550
+ span.style.whiteSpace = "nowrap";
1551
+ span.style.fontFamily = familyForMeasure;
1552
+ span.style.fontWeight = String(weight || "normal");
1553
+ span.style.fontSize = `${fontSize}px`;
1554
+ span.style.lineHeight = "1";
1555
+ span.style.margin = "0";
1556
+ span.style.padding = "0";
1557
+ span.style.fontFeatureSettings = "'liga' 1";
1558
+ span.style.fontVariantLigatures = "normal";
1559
+ span.style.color = color;
1560
+ document.body.appendChild(span);
1561
+ const rect = span.getBoundingClientRect();
1562
+ const width = Math.max(1, Math.ceil(rect.width));
1563
+ const height = Math.max(1, Math.ceil(rect.height));
1564
+ document.body.removeChild(span);
1565
+ const canvas = document.createElement("canvas");
1566
+ canvas.width = width * dpr;
1567
+ canvas.height = height * dpr;
1568
+ const ctx = canvas.getContext("2d");
1569
+ ctx.scale(dpr, dpr);
1570
+ ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px ${familyForCanvas}`;
1571
+ ctx.textAlign = "left";
1572
+ ctx.textBaseline = "top";
1573
+ ctx.fillStyle = color;
1574
+ try {
1575
+ ctx.fontKerning = "normal";
1576
+ } catch {
1577
+ }
1578
+ ctx.fillText(ligatureText, 0, 0);
1579
+ return {
1580
+ dataUrl: canvas.toDataURL(),
1581
+ width,
1582
+ height
1583
+ };
1584
+ }
1585
+ async function ligatureIconToImage(cloneRoot, sourceRoot) {
1586
+ if (!(cloneRoot instanceof Element)) return 0;
1587
+ const selector = '.material-icons, [class*="material-symbols"]';
1588
+ const cloneNodes = Array.from(
1589
+ cloneRoot.querySelectorAll(selector)
1590
+ ).filter((n) => n && n.textContent && n.textContent.trim());
1591
+ if (cloneNodes.length === 0) return 0;
1592
+ const sourceNodes = sourceRoot instanceof Element ? Array.from(sourceRoot.querySelectorAll(selector)).filter((n) => n && n.textContent && n.textContent.trim()) : [];
1593
+ let replaced = 0;
1594
+ for (let i = 0; i < cloneNodes.length; i++) {
1595
+ const el = cloneNodes[i];
1596
+ const src = sourceNodes[i] || null;
1597
+ try {
1598
+ const cs = src ? getComputedStyle(src) : getComputedStyle(el);
1599
+ const family = cs.fontFamily || "Material Icons";
1600
+ if (!isMaterialFamily(family)) continue;
1601
+ const text = (src || el).textContent.trim();
1602
+ if (!text) continue;
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 || "";
1608
+ const { dataUrl, width, height } = await materialIconToImage(text, {
1609
+ family,
1610
+ weight,
1611
+ fontSize: size,
1612
+ color,
1613
+ variation,
1614
+ className
1615
+ });
1616
+ el.textContent = "";
1617
+ const img = el.ownerDocument.createElement("img");
1618
+ img.src = dataUrl;
1619
+ img.alt = text;
1620
+ img.style.height = `${size}px`;
1621
+ img.style.width = `${Math.max(1, Math.round(width / height * size))}px`;
1622
+ img.style.objectFit = "contain";
1623
+ img.style.verticalAlign = getComputedStyle(el).verticalAlign || "baseline";
1624
+ el.appendChild(img);
1625
+ replaced++;
1626
+ } catch {
1627
+ }
1628
+ }
1629
+ return replaced;
1630
+ }
1446
1631
 
1447
1632
  // src/modules/fonts.js
1448
1633
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
@@ -1792,7 +1977,7 @@ function dedupeFontFaces(cssText) {
1792
1977
  const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1793
1978
  const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1794
1979
  const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1795
- const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
1980
+ const srcRaw = (block.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
1796
1981
  const urls = extractSrcUrls(srcRaw, location.href);
1797
1982
  const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
1798
1983
  const key = [
@@ -2000,7 +2185,7 @@ async function embedCustomFonts({
2000
2185
  const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
2001
2186
  const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
2002
2187
  const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
2003
- const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
2188
+ const srcRaw = (face.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
2004
2189
  const srcUrls = extractSrcUrls(srcRaw, link.href);
2005
2190
  if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
2006
2191
  const ranges = parseUnicodeRange(urange);
@@ -3442,12 +3627,56 @@ function stripRootShadows(originalEl, cloneRoot) {
3442
3627
  } catch {
3443
3628
  }
3444
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
+ }
3445
3674
  async function captureDOM(element, options) {
3446
3675
  if (!element) throw new Error("Element cannot be null or undefined");
3447
3676
  applyCachePolicy(options.cache);
3448
3677
  const fast = options.fast;
3449
- const straighten = !!options.straighten;
3450
- const noShadows = !!options.noShadows;
3678
+ const outerTransforms = options.outerTransforms !== false;
3679
+ const outerShadows = !!options.outerShadows;
3451
3680
  let state = { element, options, plugins: options.plugins };
3452
3681
  let clone, classCSS, styleCache;
3453
3682
  let fontsCSS = "";
@@ -3460,17 +3689,29 @@ async function captureDOM(element, options) {
3460
3689
  const undoClamp = lineClamp(state.element);
3461
3690
  try {
3462
3691
  ({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
3463
- if (straighten && clone) {
3464
- rootTransform2D = normalizeRootTransforms(state.element, state.clone);
3692
+ if (!outerTransforms && clone) {
3693
+ rootTransform2D = normalizeRootTransforms(state.element, clone);
3465
3694
  }
3466
- if (noShadows && clone) {
3467
- stripRootShadows(state.element, state.clone);
3695
+ if (!outerShadows && clone) {
3696
+ stripRootShadows(state.element, clone);
3468
3697
  }
3469
3698
  } finally {
3470
3699
  undoClamp();
3471
3700
  }
3472
3701
  state = { clone, classCSS, styleCache, ...state };
3473
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
+ }
3711
+ try {
3712
+ await ligatureIconToImage(state.clone, state.element);
3713
+ } catch {
3714
+ }
3474
3715
  await new Promise((resolve) => {
3475
3716
  idle(async () => {
3476
3717
  await inlineImages(state.clone, state.options);
@@ -3524,8 +3765,15 @@ async function captureDOM(element, options) {
3524
3765
  idle(() => {
3525
3766
  const csEl = getComputedStyle(state.element);
3526
3767
  const rect = state.element.getBoundingClientRect();
3527
- const w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3528
- 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
+ }
3529
3777
  const coerceNum = (v, def = NaN) => {
3530
3778
  const n = typeof v === "string" ? parseFloat(v) : v;
3531
3779
  return Number.isFinite(n) ? n : def;
@@ -3550,7 +3798,7 @@ async function captureDOM(element, options) {
3550
3798
  h = h0;
3551
3799
  }
3552
3800
  let minX = 0, minY = 0, maxX = w0, maxY = h0;
3553
- if (straighten && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3801
+ if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3554
3802
  const M2 = {
3555
3803
  a: rootTransform2D.a,
3556
3804
  b: rootTransform2D.b || 0,
@@ -3565,7 +3813,7 @@ async function captureDOM(element, options) {
3565
3813
  maxX = limitDecimals(bb2.maxX);
3566
3814
  maxY = limitDecimals(bb2.maxY);
3567
3815
  } else {
3568
- const useTFBBox = !straighten && hasTFBBox(state.element);
3816
+ const useTFBBox = outerTransforms && hasTFBBox(state.element);
3569
3817
  if (useTFBBox) {
3570
3818
  const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3571
3819
  const ind2 = readIndividualTransforms(state.element);
@@ -3588,7 +3836,7 @@ async function captureDOM(element, options) {
3588
3836
  const bleedBlur = parseFilterBlur(csEl);
3589
3837
  const bleedOutline = parseOutline(csEl);
3590
3838
  const drop = parseFilterDropShadows(csEl);
3591
- const bleed = noShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3839
+ const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3592
3840
  top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
3593
3841
  right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
3594
3842
  bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
@@ -3606,7 +3854,7 @@ async function captureDOM(element, options) {
3606
3854
  const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
3607
3855
  const svgNS = "http://www.w3.org/2000/svg";
3608
3856
  const basePad = isSafari() ? 1 : 0;
3609
- const extraPad = straighten ? 1 : 0;
3857
+ const extraPad = !outerTransforms ? 1 : 0;
3610
3858
  const pad = limitDecimals(basePad + extraPad);
3611
3859
  const fo = document.createElementNS(svgNS, "foreignObject");
3612
3860
  const vbMinX = limitDecimals(minX);
@@ -3648,6 +3896,57 @@ async function captureDOM(element, options) {
3648
3896
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3649
3897
  return state.dataURL;
3650
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
+ }
3651
3950
  var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
3652
3951
  function parseFilterDropShadows(cs) {
3653
3952
  const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
@@ -3914,6 +4213,52 @@ function readTotalTransformMatrix(t) {
3914
4213
  host.removeChild(tmp);
3915
4214
  return M;
3916
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
+ }
3917
4262
 
3918
4263
  // src/core/context.js
3919
4264
  function normalizeCachePolicy(v) {
@@ -3963,8 +4308,8 @@ function createContext(options = {}) {
3963
4308
  backgroundColor: options.backgroundColor ?? (["jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
3964
4309
  filename: options.filename ?? "snapDOM",
3965
4310
  // NEW flags (user-friendly)
3966
- straighten: options.straighten ?? false,
3967
- noShadows: options.noShadows ?? false
4311
+ outerTransforms: options.outerTransforms ?? true,
4312
+ outerShadows: options.outerShadows ?? false
3968
4313
  // Plugins (reservado)
3969
4314
  // plugins: normalizePlugins(...),
3970
4315
  };
@@ -4070,7 +4415,7 @@ function maybeConvertBoxShadowForSafari(url) {
4070
4415
  }
4071
4416
  }
4072
4417
  async function toCanvas(url, options) {
4073
- let { width: optW, height: optH, scale = 1, dpr = 1, meta = {} } = options;
4418
+ let { width: optW, height: optH, scale = 1, dpr = 1, meta = {}, backgroundColor } = options;
4074
4419
  url = maybeConvertBoxShadowForSafari(url);
4075
4420
  const img = new Image();
4076
4421
  img.loading = "eager";
@@ -4109,6 +4454,12 @@ async function toCanvas(url, options) {
4109
4454
  canvas.style.height = `${outH}px`;
4110
4455
  const ctx = canvas.getContext("2d");
4111
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
+ }
4112
4463
  ctx.drawImage(img, 0, 0, outW, outH);
4113
4464
  return canvas;
4114
4465
  }
@@ -4116,12 +4467,11 @@ async function toCanvas(url, options) {
4116
4467
  // src/modules/rasterize.js
4117
4468
  async function rasterize(url, options) {
4118
4469
  const canvas = await toCanvas(url, options);
4119
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4120
4470
  const img = new Image();
4121
- img.src = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4471
+ img.src = canvas.toDataURL(`image/${options.format}`, options.quality);
4122
4472
  await img.decode();
4123
- img.style.width = `${finalCanvas.width / options.dpr}px`;
4124
- 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`;
4125
4475
  return img;
4126
4476
  }
4127
4477
 
@@ -4181,13 +4531,8 @@ async function toBlob(url, options) {
4181
4531
  return new Blob([svgText], { type: "image/svg+xml" });
4182
4532
  }
4183
4533
  const canvas = await toCanvas(url, options);
4184
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4185
4534
  return new Promise(
4186
- (resolve) => finalCanvas.toBlob(
4187
- (blob) => resolve(blob),
4188
- `image/${type}`,
4189
- options.quality
4190
- )
4535
+ (resolve) => canvas.toBlob((blob) => resolve(blob), `image/${type}`, options.quality)
4191
4536
  );
4192
4537
  }
4193
4538
 
@@ -4205,9 +4550,8 @@ async function download(url, options) {
4205
4550
  return;
4206
4551
  }
4207
4552
  const canvas = await toCanvas(url, options);
4208
- const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
4209
4553
  const a = document.createElement("a");
4210
- a.href = finalCanvas.toDataURL(`image/${options.format}`, options.quality);
4554
+ a.href = canvas.toDataURL(`image/${options.format}`, options.quality);
4211
4555
  a.download = options.filename;
4212
4556
  a.click();
4213
4557
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "2.0.0-dev.0",
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",
@@ -78,13 +78,13 @@ export interface SnapdomOptions {
78
78
  /** How to apply `filter` ("hide" or "remove"). Default "hide". */
79
79
  filterMode?: "hide" | "remove";
80
80
 
81
- /** Straighten the root: remove translate/rotate, keep scale/skew. */
82
- straighten?: boolean;
81
+ /** outerTransforms the root: remove translate/rotate, keep scale/skew. */
82
+ outerTransforms?: boolean;
83
83
  /**
84
84
  * Do not expand root bbox for shadows/blur/outline; also strip shadows/outline
85
85
  * from the cloned root to get a tight capture box.
86
86
  */
87
- noShadows?: boolean;
87
+ outerShadows?: boolean;
88
88
 
89
89
  /** Inline non-icon fonts actually used within the subtree. */
90
90
  embedFonts?: boolean;