@zumer/snapdom 1.9.12-dev.4 → 1.9.14

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.1.9.12-dev.4
4
+ * v.1.9.14
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -807,6 +807,64 @@ function stripHeightForWrappers(el, cs, snap) {
807
807
  delete snap["block-size"];
808
808
  }
809
809
 
810
+ // src/modules/CSSVar.js
811
+ function resolveCSSVars(sourceEl, cloneEl) {
812
+ if (!(sourceEl instanceof Element) || !(cloneEl instanceof Element)) return;
813
+ const styleAttr = sourceEl.getAttribute?.("style");
814
+ let hasVar = !!(styleAttr && styleAttr.includes("var("));
815
+ if (!hasVar && sourceEl.attributes?.length) {
816
+ const attrs = sourceEl.attributes;
817
+ for (let i = 0; i < attrs.length; i++) {
818
+ const a = attrs[i];
819
+ if (a && typeof a.value === "string" && a.value.includes("var(")) {
820
+ hasVar = true;
821
+ break;
822
+ }
823
+ }
824
+ }
825
+ if (!hasVar) return;
826
+ let cs;
827
+ try {
828
+ cs = getComputedStyle(sourceEl);
829
+ } catch {
830
+ return;
831
+ }
832
+ const author = sourceEl.style;
833
+ if (author && author.length) {
834
+ for (let i = 0; i < author.length; i++) {
835
+ const prop = author[i];
836
+ const val = author.getPropertyValue(prop);
837
+ if (!val || !val.includes("var(")) continue;
838
+ const resolved = cs.getPropertyValue(prop);
839
+ if (resolved) {
840
+ try {
841
+ cloneEl.style.setProperty(prop, resolved.trim(), author.getPropertyPriority(prop));
842
+ } catch {
843
+ }
844
+ }
845
+ }
846
+ }
847
+ if (sourceEl.attributes?.length) {
848
+ const attrs = sourceEl.attributes;
849
+ for (let i = 0; i < attrs.length; i++) {
850
+ const a = attrs[i];
851
+ if (!a || typeof a.value !== "string" || !a.value.includes("var(")) continue;
852
+ const propName = a.name;
853
+ let resolved = "";
854
+ try {
855
+ resolved = cs.getPropertyValue(propName);
856
+ } catch {
857
+ }
858
+ if (resolved) {
859
+ try {
860
+ cloneEl.style.setProperty(propName, resolved.trim());
861
+ } catch {
862
+ }
863
+ }
864
+ }
865
+ }
866
+ }
867
+
810
868
  // src/core/clone.js
811
869
  function idleCallback(childList, callback, fast) {
812
870
  return Promise.all(childList.map((child) => {
@@ -828,7 +886,7 @@ function idleCallback(childList, callback, fast) {
828
886
  function addNotSlottedRightmost(sel) {
829
887
  sel = sel.trim();
830
888
  if (!sel) return sel;
831
- if (/\:not\(\s*\[data-sd-slotted\]\s*\)\s*$/.test(sel)) return sel;
889
+ if (/:not\(\s*\[data-sd-slotted\]\s*\)\s*$/.test(sel)) return sel;
832
890
  return `${sel}:not([data-sd-slotted])`;
833
891
  }
834
892
  function wrapWithScope(selectorList, scopeSelector, excludeSlotted = true) {
@@ -1117,7 +1175,7 @@ async function deepClone(node, sessionCache, options) {
1117
1175
  inlineAllStyles(node, clone2, sessionCache, options);
1118
1176
  const placeholder = document.createElement("div");
1119
1177
  placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
1120
- placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
1178
+ placeholder.style.cssText = "color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;";
1121
1179
  clone2.appendChild(placeholder);
1122
1180
  return clone2;
1123
1181
  }
@@ -1134,6 +1192,7 @@ async function deepClone(node, sessionCache, options) {
1134
1192
  let clone;
1135
1193
  try {
1136
1194
  clone = node.cloneNode(false);
1195
+ resolveCSSVars(node, clone);
1137
1196
  sessionCache.nodeMap.set(clone, node);
1138
1197
  if (node.tagName === "IMG") {
1139
1198
  freezeImgSrcset(node, clone);
@@ -1189,7 +1248,7 @@ async function deepClone(node, sessionCache, options) {
1189
1248
  } else {
1190
1249
  deepClone(child, sessionCache, options).then((clonedChild) => {
1191
1250
  resolve(clonedChild || null);
1192
- }).catch((e) => {
1251
+ }).catch(() => {
1193
1252
  resolve(null);
1194
1253
  });
1195
1254
  }
@@ -1230,7 +1289,7 @@ async function deepClone(node, sessionCache, options) {
1230
1289
  markSlottedSubtree(clonedChild);
1231
1290
  }
1232
1291
  resolve(clonedChild || null);
1233
- }).catch((e) => {
1292
+ }).catch(() => {
1234
1293
  resolve(null);
1235
1294
  });
1236
1295
  };
@@ -1245,7 +1304,7 @@ async function deepClone(node, sessionCache, options) {
1245
1304
  if (clonedAssignedNodes.has(child)) return resolve(null);
1246
1305
  deepClone(child, sessionCache, options).then((clonedChild) => {
1247
1306
  resolve(clonedChild || null);
1248
- }).catch((e) => {
1307
+ }).catch(() => {
1249
1308
  resolve(null);
1250
1309
  });
1251
1310
  }
@@ -1308,7 +1367,10 @@ function isIconFont2(input) {
1308
1367
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
1309
1368
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
1310
1369
  const dpr = window.devicePixelRatio || 1;
1311
- await document.fonts.ready;
1370
+ try {
1371
+ await document.fonts.ready;
1372
+ } catch {
1373
+ }
1312
1374
  const span = document.createElement("span");
1313
1375
  span.textContent = unicodeChar;
1314
1376
  span.style.position = "absolute";
@@ -1326,8 +1388,8 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
1326
1388
  const height = Math.ceil(rect.height);
1327
1389
  document.body.removeChild(span);
1328
1390
  const canvas = document.createElement("canvas");
1329
- canvas.width = width * dpr;
1330
- canvas.height = height * dpr;
1391
+ canvas.width = Math.max(1, width * dpr);
1392
+ canvas.height = Math.max(1, height * dpr);
1331
1393
  const ctx = canvas.getContext("2d");
1332
1394
  ctx.scale(dpr, dpr);
1333
1395
  ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
@@ -1445,6 +1507,303 @@ function familiesFromRequired(required) {
1445
1507
  }
1446
1508
  return out;
1447
1509
  }
1510
+ function rewriteRelativeUrls(cssText, baseHref) {
1511
+ if (!cssText) return cssText;
1512
+ return cssText.replace(
1513
+ /url\(\s*(['"]?)([^)'"]+)\1\s*\)/g,
1514
+ (m, q, u) => {
1515
+ const src = (u || "").trim();
1516
+ if (!src || /^data:|^blob:|^https?:|^file:|^about:/i.test(src)) return m;
1517
+ let abs = src;
1518
+ try {
1519
+ abs = new URL(src, baseHref || location.href).href;
1520
+ } catch {
1521
+ }
1522
+ return `url("${abs}")`;
1523
+ }
1524
+ );
1525
+ }
1526
+ var IMPORT_ANY_RE = /@import\s+(?:url\(\s*(['"]?)([^)"']+)\1\s*\)|(['"])([^"']+)\3)([^;]*);/g;
1527
+ var MAX_IMPORT_DEPTH = 4;
1528
+ async function inlineImportsAndRewrite(cssText, ownerHref, useProxy) {
1529
+ if (!cssText) return cssText;
1530
+ const visited = /* @__PURE__ */ new Set();
1531
+ function normalizeUrl(u, base) {
1532
+ try {
1533
+ return new URL(u, base || location.href).href;
1534
+ } catch {
1535
+ return u;
1536
+ }
1537
+ }
1538
+ async function resolveOnce(text, baseHref, depth = 0) {
1539
+ if (depth > MAX_IMPORT_DEPTH) {
1540
+ console.warn(`[snapDOM] @import depth exceeded (${MAX_IMPORT_DEPTH}) at ${baseHref}`);
1541
+ return text;
1542
+ }
1543
+ let out = "";
1544
+ let last = 0;
1545
+ let m;
1546
+ while (m = IMPORT_ANY_RE.exec(text)) {
1547
+ out += text.slice(last, m.index);
1548
+ last = IMPORT_ANY_RE.lastIndex;
1549
+ const rawUrl = (m[2] || m[4] || "").trim();
1550
+ const absUrl = normalizeUrl(rawUrl, baseHref);
1551
+ if (visited.has(absUrl)) {
1552
+ console.warn(`[snapDOM] Skipping circular @import: ${absUrl}`);
1553
+ continue;
1554
+ }
1555
+ visited.add(absUrl);
1556
+ let imported = "";
1557
+ try {
1558
+ const r = await snapFetch(absUrl, { as: "text", useProxy, silent: true });
1559
+ if (r.ok && typeof r.data === "string") imported = r.data;
1560
+ } catch {
1561
+ }
1562
+ if (imported) {
1563
+ imported = rewriteRelativeUrls(imported, absUrl);
1564
+ imported = await resolveOnce(imported, absUrl, depth + 1);
1565
+ out += `
1566
+ /* inlined: ${absUrl} */
1567
+ ${imported}
1568
+ `;
1569
+ } else {
1570
+ out += m[0];
1571
+ }
1572
+ }
1573
+ out += text.slice(last);
1574
+ return out;
1575
+ }
1576
+ let rewritten = rewriteRelativeUrls(cssText, ownerHref || location.href);
1577
+ rewritten = await resolveOnce(rewritten, ownerHref || location.href, 0);
1578
+ return rewritten;
1579
+ }
1580
+ var URL_RE = /url\((["']?)([^"')]+)\1\)/g;
1581
+ var FACE_RE = /@font-face[^{}]*\{[^}]*\}/g;
1582
+ function parseUnicodeRange(ur) {
1583
+ if (!ur) return [];
1584
+ const ranges = [];
1585
+ const parts = ur.split(",").map((s) => s.trim()).filter(Boolean);
1586
+ for (const p of parts) {
1587
+ const m = p.match(/^U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f?]+))?$/);
1588
+ if (!m) continue;
1589
+ const a = m[1], b = m[2];
1590
+ const expand = (hex) => {
1591
+ if (!hex.includes("?")) return parseInt(hex, 16);
1592
+ const min = parseInt(hex.replace(/\?/g, "0"), 16);
1593
+ const max = parseInt(hex.replace(/\?/g, "F"), 16);
1594
+ return [min, max];
1595
+ };
1596
+ if (b) {
1597
+ const A = expand(a), B = expand(b);
1598
+ const min = Array.isArray(A) ? A[0] : A;
1599
+ const max = Array.isArray(B) ? B[1] : B;
1600
+ ranges.push([Math.min(min, max), Math.max(min, max)]);
1601
+ } else {
1602
+ const X = expand(a);
1603
+ if (Array.isArray(X)) ranges.push([X[0], X[1]]);
1604
+ else ranges.push([X, X]);
1605
+ }
1606
+ }
1607
+ return ranges;
1608
+ }
1609
+ function unicodeIntersects(used, ranges) {
1610
+ if (!ranges.length) return true;
1611
+ if (!used || used.size === 0) return true;
1612
+ for (const cp of used) {
1613
+ for (const [a, b] of ranges) if (cp >= a && cp <= b) return true;
1614
+ }
1615
+ return false;
1616
+ }
1617
+ function extractSrcUrls(srcValue, baseHref) {
1618
+ const urls = [];
1619
+ if (!srcValue) return urls;
1620
+ for (const m of srcValue.matchAll(URL_RE)) {
1621
+ let u = (m[2] || "").trim();
1622
+ if (!u || u.startsWith("data:")) continue;
1623
+ if (!/^https?:/i.test(u)) {
1624
+ try {
1625
+ u = new URL(u, baseHref || location.href).href;
1626
+ } catch {
1627
+ }
1628
+ }
1629
+ urls.push(u);
1630
+ }
1631
+ return urls;
1632
+ }
1633
+ async function inlineUrlsInCssBlock(cssBlock, baseHref, useProxy = "") {
1634
+ let out = cssBlock;
1635
+ for (const m of cssBlock.matchAll(URL_RE)) {
1636
+ const raw = extractURL(m[0]);
1637
+ if (!raw) continue;
1638
+ let abs = raw;
1639
+ if (!abs.startsWith("http") && !abs.startsWith("data:")) {
1640
+ try {
1641
+ abs = new URL(abs, baseHref || location.href).href;
1642
+ } catch {
1643
+ }
1644
+ }
1645
+ if (isIconFont2(abs)) continue;
1646
+ if (cache.resource?.has(abs)) {
1647
+ cache.font?.add(abs);
1648
+ out = out.replace(m[0], `url(${cache.resource.get(abs)})`);
1649
+ continue;
1650
+ }
1651
+ if (cache.font?.has(abs)) continue;
1652
+ try {
1653
+ const r = await snapFetch(abs, { as: "dataURL", useProxy, silent: true });
1654
+ if (r.ok && typeof r.data === "string") {
1655
+ const b64 = r.data;
1656
+ cache.resource?.set(abs, b64);
1657
+ cache.font?.add(abs);
1658
+ out = out.replace(m[0], `url(${b64})`);
1659
+ }
1660
+ } catch {
1661
+ console.warn("[snapDOM] Failed to fetch font resource:", abs);
1662
+ }
1663
+ }
1664
+ return out;
1665
+ }
1666
+ function subsetFromRanges(ranges) {
1667
+ if (!ranges.length) return null;
1668
+ const hit = (a, b) => ranges.some(([x, y]) => !(y < a || x > b));
1669
+ const latin = hit(0, 255) || hit(305, 305);
1670
+ const latinExt = hit(256, 591) || hit(7680, 7935);
1671
+ const greek = hit(880, 1023);
1672
+ const cyr = hit(1024, 1279);
1673
+ const viet = hit(7840, 7929) || hit(258, 259) || hit(416, 417) || hit(431, 432);
1674
+ if (viet) return "vietnamese";
1675
+ if (cyr) return "cyrillic";
1676
+ if (greek) return "greek";
1677
+ if (latinExt) return "latin-ext";
1678
+ if (latin) return "latin";
1679
+ return null;
1680
+ }
1681
+ function buildSimpleExcluder(ex = {}) {
1682
+ const famSet = new Set((ex.families || []).map((s) => String(s).toLowerCase()));
1683
+ const domSet = new Set((ex.domains || []).map((s) => String(s).toLowerCase()));
1684
+ const subSet = new Set((ex.subsets || []).map((s) => String(s).toLowerCase()));
1685
+ return (meta, parsedRanges) => {
1686
+ if (famSet.size && famSet.has(meta.family.toLowerCase())) return true;
1687
+ if (domSet.size) {
1688
+ for (const u of meta.srcUrls) {
1689
+ try {
1690
+ if (domSet.has(new URL(u).host.toLowerCase())) return true;
1691
+ } catch {
1692
+ }
1693
+ }
1694
+ }
1695
+ if (subSet.size) {
1696
+ const label = subsetFromRanges(parsedRanges);
1697
+ if (label && subSet.has(label)) return true;
1698
+ }
1699
+ return false;
1700
+ };
1701
+ }
1702
+ function dedupeFontFaces(cssText) {
1703
+ if (!cssText) return cssText;
1704
+ const FACE_RE_G = /@font-face[^{}]*\{[^}]*\}/gi;
1705
+ const seen = /* @__PURE__ */ new Set();
1706
+ const out = [];
1707
+ for (const block of cssText.match(FACE_RE_G) || []) {
1708
+ const familyRaw = block.match(/font-family:\s*([^;]+);/i)?.[1] || "";
1709
+ const family = pickPrimaryFamily(familyRaw);
1710
+ const weightSpec = (block.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
1711
+ const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1712
+ const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1713
+ const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1714
+ const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
1715
+ const urls = extractSrcUrls(srcRaw, location.href);
1716
+ const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
1717
+ const key = [
1718
+ String(family || "").toLowerCase(),
1719
+ weightSpec,
1720
+ styleSpec,
1721
+ stretchSpec,
1722
+ urange.toLowerCase(),
1723
+ srcPart
1724
+ ].join("|");
1725
+ if (!seen.has(key)) {
1726
+ seen.add(key);
1727
+ out.push(block);
1728
+ }
1729
+ }
1730
+ if (out.length === 0) return cssText;
1731
+ let i = 0;
1732
+ return cssText.replace(FACE_RE_G, () => out[i++] || "");
1733
+ }
1734
+ function buildFontsCacheKey(required, exclude, localFonts, useProxy) {
1735
+ const req = Array.from(required || []).sort().join("|");
1736
+ const ex = exclude ? JSON.stringify({
1737
+ families: (exclude.families || []).map((s) => String(s).toLowerCase()).sort(),
1738
+ domains: (exclude.domains || []).map((s) => String(s).toLowerCase()).sort(),
1739
+ subsets: (exclude.subsets || []).map((s) => String(s).toLowerCase()).sort()
1740
+ }) : "";
1741
+ const lf = (localFonts || []).map((f) => `${(f.family || "").toLowerCase()}::${f.weight || "normal"}::${f.style || "normal"}::${f.src || ""}`).sort().join("|");
1742
+ const px = useProxy || "";
1743
+ return `fonts-embed-css::req=${req}::ex=${ex}::lf=${lf}::px=${px}`;
1744
+ }
1745
+ async function collectFacesFromSheet(sheet, baseHref, emitFace, ctx) {
1746
+ let rules;
1747
+ try {
1748
+ rules = sheet.cssRules || [];
1749
+ } catch {
1750
+ return;
1751
+ }
1752
+ const normalizeUrl = (u, base) => {
1753
+ try {
1754
+ return new URL(u, base || location.href).href;
1755
+ } catch {
1756
+ return u;
1757
+ }
1758
+ };
1759
+ for (const rule of rules) {
1760
+ if (rule.type === CSSRule.IMPORT_RULE && rule.styleSheet) {
1761
+ const childHref = rule.href ? normalizeUrl(rule.href, baseHref) : baseHref;
1762
+ if (ctx.depth >= MAX_IMPORT_DEPTH) {
1763
+ console.warn(`[snapDOM] CSSOM import depth exceeded (${MAX_IMPORT_DEPTH}) at ${childHref}`);
1764
+ continue;
1765
+ }
1766
+ if (childHref && ctx.visitedSheets.has(childHref)) {
1767
+ console.warn(`[snapDOM] Skipping circular CSSOM import: ${childHref}`);
1768
+ continue;
1769
+ }
1770
+ if (childHref) ctx.visitedSheets.add(childHref);
1771
+ const nextCtx = { ...ctx, depth: (ctx.depth || 0) + 1 };
1772
+ await collectFacesFromSheet(rule.styleSheet, childHref, emitFace, nextCtx);
1773
+ continue;
1774
+ }
1775
+ if (rule.type === CSSRule.FONT_FACE_RULE) {
1776
+ const famRaw = (rule.style.getPropertyValue("font-family") || "").trim();
1777
+ const family = pickPrimaryFamily(famRaw);
1778
+ if (!family || isIconFont2(family)) continue;
1779
+ const weightSpec = (rule.style.getPropertyValue("font-weight") || "400").trim();
1780
+ const styleSpec = (rule.style.getPropertyValue("font-style") || "normal").trim();
1781
+ const stretchSpec = (rule.style.getPropertyValue("font-stretch") || "100%").trim();
1782
+ const srcRaw = (rule.style.getPropertyValue("src") || "").trim();
1783
+ const urange = (rule.style.getPropertyValue("unicode-range") || "").trim();
1784
+ if (!ctx.faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
1785
+ const ranges = parseUnicodeRange(urange);
1786
+ if (!unicodeIntersects(ctx.usedCodepoints, ranges)) continue;
1787
+ const meta = {
1788
+ family,
1789
+ weightSpec,
1790
+ styleSpec,
1791
+ stretchSpec,
1792
+ unicodeRange: urange,
1793
+ srcRaw,
1794
+ srcUrls: extractSrcUrls(srcRaw, baseHref || location.href),
1795
+ href: baseHref || location.href
1796
+ };
1797
+ if (ctx.simpleExcluder && ctx.simpleExcluder(meta, ranges)) continue;
1798
+ if (/url\(/i.test(srcRaw)) {
1799
+ const inlinedSrc = await inlineUrlsInCssBlock(srcRaw, baseHref || location.href, ctx.useProxy);
1800
+ await emitFace(`@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`);
1801
+ } else {
1802
+ await emitFace(`@font-face{font-family:${family};src:${srcRaw};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`);
1803
+ }
1804
+ }
1805
+ }
1806
+ }
1448
1807
  async function embedCustomFonts({
1449
1808
  required,
1450
1809
  usedCodepoints,
@@ -1462,9 +1821,6 @@ async function embedCustomFonts({
1462
1821
  arr.push({ w: parseInt(w, 10), s, st: parseInt(st, 10) });
1463
1822
  requiredIndex.set(fam, arr);
1464
1823
  }
1465
- const IMPORT_RE = /@import\s+url\(["']?([^"')]+)["']?\)/g;
1466
- const URL_RE = /url\((["']?)([^"')]+)\1\)/g;
1467
- const FACE_RE = /@font-face[^{}]*\{[^}]*\}/g;
1468
1824
  function faceMatchesRequired(fam, styleSpec, weightSpec, stretchSpec) {
1469
1825
  if (!requiredIndex.has(fam)) return false;
1470
1826
  const need = requiredIndex.get(fam);
@@ -1490,188 +1846,23 @@ async function embedCustomFonts({
1490
1846
  const sOk = styleOK(normStyle(r.s));
1491
1847
  const tOk = r.st >= ts.min && r.st <= ts.max;
1492
1848
  const nearWeight = Math.abs(faceSingleW - r.w) <= 300;
1493
- if (nearWeight && sOk && tOk) {
1494
- return true;
1495
- }
1849
+ if (nearWeight && sOk && tOk) return true;
1496
1850
  }
1497
1851
  }
1498
1852
  return false;
1499
1853
  }
1500
- function parseUnicodeRange(ur) {
1501
- if (!ur) return [];
1502
- const ranges = [];
1503
- const parts = ur.split(",").map((s) => s.trim()).filter(Boolean);
1504
- for (const p of parts) {
1505
- const m = p.match(/^U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f?]+))?$/);
1506
- if (!m) continue;
1507
- const a = m[1], b = m[2];
1508
- const expand = (hex) => {
1509
- if (!hex.includes("?")) return parseInt(hex, 16);
1510
- const min = parseInt(hex.replace(/\?/g, "0"), 16);
1511
- const max = parseInt(hex.replace(/\?/g, "F"), 16);
1512
- return [min, max];
1513
- };
1514
- if (b) {
1515
- const A = expand(a), B = expand(b);
1516
- const min = Array.isArray(A) ? A[0] : A;
1517
- const max = Array.isArray(B) ? B[1] : B;
1518
- ranges.push([Math.min(min, max), Math.max(min, max)]);
1519
- } else {
1520
- const X = expand(a);
1521
- if (Array.isArray(X)) ranges.push([X[0], X[1]]);
1522
- else ranges.push([X, X]);
1523
- }
1524
- }
1525
- return ranges;
1526
- }
1527
- function unicodeIntersects(used, ranges) {
1528
- if (!ranges.length) return true;
1529
- if (!used || used.size === 0) return true;
1530
- for (const cp of used) {
1531
- for (const [a, b] of ranges) if (cp >= a && cp <= b) return true;
1532
- }
1533
- return false;
1534
- }
1535
- function extractSrcUrls(srcValue, baseHref) {
1536
- const urls = [];
1537
- if (!srcValue) return urls;
1538
- for (const m of srcValue.matchAll(URL_RE)) {
1539
- let u = (m[2] || "").trim();
1540
- if (!u || u.startsWith("data:")) continue;
1541
- if (!/^https?:/i.test(u)) {
1542
- try {
1543
- u = new URL(u, baseHref || location.href).href;
1544
- } catch {
1545
- }
1546
- }
1547
- urls.push(u);
1548
- }
1549
- return urls;
1550
- }
1551
- async function inlineUrlsInCssBlock(cssBlock, baseHref) {
1552
- let out = cssBlock;
1553
- for (const m of cssBlock.matchAll(URL_RE)) {
1554
- const raw = extractURL(m[0]);
1555
- if (!raw) continue;
1556
- let abs = raw;
1557
- if (!abs.startsWith("http") && !abs.startsWith("data:")) {
1558
- try {
1559
- abs = new URL(abs, baseHref || location.href).href;
1560
- } catch {
1561
- }
1562
- }
1563
- if (isIconFont2(abs)) continue;
1564
- if (cache.resource?.has(abs)) {
1565
- cache.font?.add(abs);
1566
- out = out.replace(m[0], `url(${cache.resource.get(abs)})`);
1567
- continue;
1568
- }
1569
- if (cache.font?.has(abs)) continue;
1570
- try {
1571
- const r = await snapFetch(abs, { as: "dataURL", useProxy, silent: true });
1572
- if (r.ok && typeof r.data === "string") {
1573
- const b64 = r.data;
1574
- cache.resource?.set(abs, b64);
1575
- cache.font?.add(abs);
1576
- out = out.replace(m[0], `url(${b64})`);
1577
- }
1578
- } catch {
1579
- console.warn("[snapDOM] Failed to fetch font resource:", abs);
1580
- }
1581
- }
1582
- return out;
1583
- }
1584
- function subsetFromRanges(ranges) {
1585
- if (!ranges.length) return null;
1586
- const hit = (a, b) => ranges.some(([x, y]) => !(y < a || x > b));
1587
- const latin = hit(0, 255) || hit(305, 305);
1588
- const latinExt = hit(256, 591) || hit(7680, 7935);
1589
- const greek = hit(880, 1023);
1590
- const cyr = hit(1024, 1279);
1591
- const viet = hit(7840, 7929) || hit(258, 259) || hit(416, 417) || hit(431, 432);
1592
- if (viet) return "vietnamese";
1593
- if (cyr) return "cyrillic";
1594
- if (greek) return "greek";
1595
- if (latinExt) return "latin-ext";
1596
- if (latin) return "latin";
1597
- return null;
1598
- }
1599
- function buildSimpleExcluder(ex = {}) {
1600
- const famSet = new Set((ex.families || []).map((s) => String(s).toLowerCase()));
1601
- const domSet = new Set((ex.domains || []).map((s) => String(s).toLowerCase()));
1602
- const subSet = new Set((ex.subsets || []).map((s) => String(s).toLowerCase()));
1603
- return (meta, parsedRanges) => {
1604
- if (famSet.size && famSet.has(meta.family.toLowerCase())) return true;
1605
- if (domSet.size) {
1606
- for (const u of meta.srcUrls) {
1607
- try {
1608
- if (domSet.has(new URL(u).host.toLowerCase())) return true;
1609
- } catch {
1610
- }
1611
- }
1612
- }
1613
- if (subSet.size) {
1614
- const label = subsetFromRanges(parsedRanges);
1615
- if (label && subSet.has(label)) return true;
1616
- }
1617
- return false;
1618
- };
1619
- }
1620
1854
  const simpleExcluder = buildSimpleExcluder(exclude);
1621
- function dedupeFontFaces(cssText) {
1622
- if (!cssText) return cssText;
1623
- const FACE_RE2 = /@font-face[^{}]*\{[^}]*\}/gi;
1624
- const seen = /* @__PURE__ */ new Set();
1625
- const out = [];
1626
- for (const block of cssText.match(FACE_RE2) || []) {
1627
- const familyRaw = block.match(/font-family:\s*([^;]+);/i)?.[1] || "";
1628
- const family = pickPrimaryFamily(familyRaw);
1629
- const weightSpec = (block.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
1630
- const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1631
- const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1632
- const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1633
- const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
1634
- const urls = extractSrcUrls(srcRaw, location.href);
1635
- const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
1636
- const key = [
1637
- String(family || "").toLowerCase(),
1638
- weightSpec,
1639
- styleSpec,
1640
- stretchSpec,
1641
- urange.toLowerCase(),
1642
- srcPart
1643
- ].join("|");
1644
- if (!seen.has(key)) {
1645
- seen.add(key);
1646
- out.push(block);
1647
- }
1648
- }
1649
- if (out.length === 0) return cssText;
1650
- let i = 0;
1651
- return cssText.replace(FACE_RE2, () => out[i++] || "");
1652
- }
1653
- function buildFontsCacheKey() {
1654
- const req = Array.from(required || []).sort().join("|");
1655
- const ex = exclude ? JSON.stringify({
1656
- families: (exclude.families || []).map((s) => String(s).toLowerCase()).sort(),
1657
- domains: (exclude.domains || []).map((s) => String(s).toLowerCase()).sort(),
1658
- subsets: (exclude.subsets || []).map((s) => String(s).toLowerCase()).sort()
1659
- }) : "";
1660
- const lf = (localFonts || []).map((f) => `${(f.family || "").toLowerCase()}::${f.weight || "normal"}::${f.style || "normal"}::${f.src || ""}`).sort().join("|");
1661
- const px = useProxy || "";
1662
- return `fonts-embed-css::req=${req}::ex=${ex}::lf=${lf}::px=${px}`;
1663
- }
1664
- const cacheKey = buildFontsCacheKey();
1855
+ const cacheKey = buildFontsCacheKey(required, exclude, localFonts, useProxy);
1665
1856
  if (cache.resource?.has(cacheKey)) {
1666
- const css = cache.resource.get(cacheKey);
1667
- return css;
1857
+ return cache.resource.get(cacheKey);
1668
1858
  }
1669
1859
  const requiredFamilies = familiesFromRequired(required);
1670
1860
  const importUrls = [];
1861
+ const IMPORT_ANY_RE_LOCAL = IMPORT_ANY_RE;
1671
1862
  for (const styleTag of document.querySelectorAll("style")) {
1672
1863
  const cssText = styleTag.textContent || "";
1673
- for (const m of cssText.matchAll(IMPORT_RE)) {
1674
- const u = m[1];
1864
+ for (const m of cssText.matchAll(IMPORT_ANY_RE_LOCAL)) {
1865
+ const u = (m[2] || m[4] || "").trim();
1675
1866
  if (!u || isIconFont2(u)) continue;
1676
1867
  const hasLink = !!document.querySelector(`link[rel="stylesheet"][href="${u}"]`);
1677
1868
  if (!hasLink) importUrls.push(u);
@@ -1718,65 +1909,53 @@ async function embedCustomFonts({
1718
1909
  cssText = res.data;
1719
1910
  if (isIconFont2(link.href)) continue;
1720
1911
  }
1912
+ cssText = await inlineImportsAndRewrite(cssText, link.href, useProxy);
1721
1913
  let facesOut = "";
1722
1914
  for (const face of cssText.match(FACE_RE) || []) {
1723
1915
  const famRaw = (face.match(/font-family:\s*([^;]+);/i)?.[1] || "").trim();
1724
1916
  const family = pickPrimaryFamily(famRaw);
1725
- if (!family || isIconFont2(family)) {
1726
- continue;
1727
- }
1917
+ if (!family || isIconFont2(family)) continue;
1728
1918
  const weightSpec = (face.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
1729
1919
  const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
1730
1920
  const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
1731
1921
  const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
1732
1922
  const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
1733
1923
  const srcUrls = extractSrcUrls(srcRaw, link.href);
1734
- if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) {
1735
- continue;
1736
- }
1924
+ if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
1737
1925
  const ranges = parseUnicodeRange(urange);
1738
- if (!unicodeIntersects(usedCodepoints, ranges)) {
1739
- continue;
1740
- }
1926
+ if (!unicodeIntersects(usedCodepoints, ranges)) continue;
1741
1927
  const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: link.href };
1742
- if (exclude && simpleExcluder(meta, ranges)) {
1743
- continue;
1744
- }
1745
- const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href) : face;
1746
- facesOut += newFace + "";
1928
+ if (exclude && simpleExcluder(meta, ranges)) continue;
1929
+ const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href, useProxy) : face;
1930
+ facesOut += newFace;
1747
1931
  }
1748
- if (facesOut.trim()) finalCSS += facesOut + "";
1749
- } catch (e) {
1750
- console.warn("[snapdom] Failed to process stylesheet:", link.href);
1932
+ if (facesOut.trim()) finalCSS += facesOut;
1933
+ } catch {
1934
+ console.warn("[snapDOM] Failed to process stylesheet:", link.href);
1751
1935
  }
1752
1936
  }
1937
+ const ctx = {
1938
+ requiredIndex,
1939
+ usedCodepoints,
1940
+ faceMatchesRequired,
1941
+ simpleExcluder: exclude ? buildSimpleExcluder(exclude) : null,
1942
+ useProxy,
1943
+ visitedSheets: /* @__PURE__ */ new Set(),
1944
+ depth: 0
1945
+ };
1753
1946
  for (const sheet of document.styleSheets) {
1947
+ if (sheet.href && linkNodes.some((l) => l.href === sheet.href)) continue;
1754
1948
  try {
1755
- if (sheet.href && linkNodes.some((l) => l.href === sheet.href)) continue;
1756
- const rules = sheet.cssRules || [];
1757
- for (const rule of rules) {
1758
- if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
1759
- const famRaw = (rule.style.getPropertyValue("font-family") || "").trim();
1760
- const family = pickPrimaryFamily(famRaw);
1761
- if (!family || isIconFont2(family)) continue;
1762
- const weightSpec = (rule.style.getPropertyValue("font-weight") || "400").trim();
1763
- const styleSpec = (rule.style.getPropertyValue("font-style") || "normal").trim();
1764
- const stretchSpec = (rule.style.getPropertyValue("font-stretch") || "100%").trim();
1765
- const srcRaw = (rule.style.getPropertyValue("src") || "").trim();
1766
- const urange = (rule.style.getPropertyValue("unicode-range") || "").trim();
1767
- const srcUrls = extractSrcUrls(srcRaw, sheet.href || location.href);
1768
- if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
1769
- const ranges = parseUnicodeRange(urange);
1770
- if (!unicodeIntersects(usedCodepoints, ranges)) continue;
1771
- const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: sheet.href || location.href };
1772
- if (exclude && simpleExcluder(meta, ranges)) continue;
1773
- if (/url\(/i.test(srcRaw)) {
1774
- const inlinedSrc = await inlineUrlsInCssBlock(srcRaw, sheet.href || location.href);
1775
- finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
1776
- } else {
1777
- finalCSS += `@font-face{font-family:${family};src:${srcRaw};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
1778
- }
1779
- }
1949
+ const rootHref = sheet.href || location.href;
1950
+ if (rootHref) ctx.visitedSheets.add(rootHref);
1951
+ await collectFacesFromSheet(
1952
+ sheet,
1953
+ rootHref,
1954
+ async (faceCss) => {
1955
+ finalCSS += faceCss;
1956
+ },
1957
+ ctx
1958
+ );
1780
1959
  } catch {
1781
1960
  }
1782
1961
  }
@@ -1805,7 +1984,7 @@ async function embedCustomFonts({
1805
1984
  continue;
1806
1985
  }
1807
1986
  } catch {
1808
- console.warn("[snapdom] Failed to fetch dynamic font src:", f._snapdomSrc);
1987
+ console.warn("[snapDOM] Failed to fetch dynamic font src:", f._snapdomSrc);
1809
1988
  continue;
1810
1989
  }
1811
1990
  }
@@ -1840,7 +2019,7 @@ async function embedCustomFonts({
1840
2019
  continue;
1841
2020
  }
1842
2021
  } catch {
1843
- console.warn("[snapdom] Failed to fetch localFonts src:", src);
2022
+ console.warn("[snapDOM] Failed to fetch localFonts src:", src);
1844
2023
  continue;
1845
2024
  }
1846
2025
  }
@@ -1929,7 +2108,7 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
1929
2108
  if (fams.length === 0) return;
1930
2109
  const warmupOnce = () => {
1931
2110
  const container = document.createElement("div");
1932
- container.style.cssText = `position: absolute !important;left: -9999px !important;top: 0 !important;opacity: 0 !important;pointer-events: none !important;contain: layout size style;`;
2111
+ container.style.cssText = "position:absolute!important;left:-9999px!important;top:0!important;opacity:0!important;pointer-events:none!important;contain:layout size style;";
1933
2112
  for (const fam of fams) {
1934
2113
  const span = document.createElement("span");
1935
2114
  span.textContent = "AaBbGg1234\xC1\xC9\xCD\xD3\xDA\xE7\xF1\u2014\u221E";
@@ -2280,9 +2459,6 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2280
2459
  const fontSize = parseInt(style.fontSize) || 32;
2281
2460
  const fontWeight = parseInt(style.fontWeight) || false;
2282
2461
  const color = style.color || "#000";
2283
- const display = style.display;
2284
- const width = parseFloat(style.width);
2285
- const height = parseFloat(style.height);
2286
2462
  const borderStyle = style.borderStyle;
2287
2463
  const borderWidth = parseFloat(style.borderWidth);
2288
2464
  const transform = style.transform;
@@ -2290,14 +2466,13 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2290
2466
  const hasExplicitContent = rawContent !== "none" && cleanContent !== "";
2291
2467
  const hasBg = bg && bg !== "none";
2292
2468
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
2293
- const hasBox2 = display !== "inline" && (width > 0 || height > 0);
2294
2469
  const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
2295
2470
  const hasTransform = transform && transform !== "none";
2296
2471
  const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBorder || hasTransform;
2297
2472
  if (!shouldRender) {
2298
2473
  if (incs && incs.length && source.parentElement) {
2299
2474
  const map = __siblingCounters.get(source.parentElement) || /* @__PURE__ */ new Map();
2300
- for (const { name, num } of incs) {
2475
+ for (const { name } of incs) {
2301
2476
  if (!name) continue;
2302
2477
  const baseWithSibs = withSiblingOverrides(source, counterCtx);
2303
2478
  const derived = deriveCounterCtxForPseudo(source, getComputedStyle(source, pseudo), baseWithSibs);
@@ -2979,18 +3154,49 @@ function isPlainTextContainer(el) {
2979
3154
  }
2980
3155
 
2981
3156
  // src/core/capture.js
3157
+ function stripRootShadows(originalEl, cloneRoot) {
3158
+ if (!originalEl || !cloneRoot || !cloneRoot.style) return;
3159
+ const cs = getComputedStyle(originalEl);
3160
+ try {
3161
+ cloneRoot.style.boxShadow = "none";
3162
+ } catch {
3163
+ }
3164
+ try {
3165
+ cloneRoot.style.textShadow = "none";
3166
+ } catch {
3167
+ }
3168
+ try {
3169
+ cloneRoot.style.outline = "none";
3170
+ } catch {
3171
+ }
3172
+ const f = cs.filter || "";
3173
+ const cleaned = f.replace(/\bblur\([^()]*\)\s*/gi, "").replace(/\bdrop-shadow\([^()]*\)\s*/gi, "").trim().replace(/\s+/g, " ");
3174
+ try {
3175
+ cloneRoot.style.filter = cleaned.length ? cleaned : "none";
3176
+ } catch {
3177
+ }
3178
+ }
2982
3179
  async function captureDOM(element, options) {
2983
3180
  if (!element) throw new Error("Element cannot be null or undefined");
2984
3181
  applyCachePolicy(options.cache);
2985
3182
  const fast = options.fast;
3183
+ const straighten = !!options.straighten;
3184
+ const noShadows = !!options.noShadows;
2986
3185
  let clone, classCSS, styleCache;
2987
3186
  let fontsCSS = "";
2988
3187
  let baseCSS = "";
2989
3188
  let dataURL;
2990
3189
  let svgString;
3190
+ let rootTransform2D = null;
2991
3191
  const undoClamp = lineClamp(element);
2992
3192
  try {
2993
3193
  ({ clone, classCSS, styleCache } = await prepareClone(element, options));
3194
+ if (straighten && clone) {
3195
+ rootTransform2D = normalizeRootTransforms(element, clone);
3196
+ }
3197
+ if (noShadows && clone) {
3198
+ stripRootShadows(element, clone);
3199
+ }
2994
3200
  } finally {
2995
3201
  undoClamp();
2996
3202
  }
@@ -3015,7 +3221,7 @@ async function captureDOM(element, options) {
3015
3221
  const families = new Set(
3016
3222
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
3017
3223
  );
3018
- await ensureFontsReady(families, 2);
3224
+ await ensureFontsReady(families, 1);
3019
3225
  }
3020
3226
  fontsCSS = await embedCustomFonts({
3021
3227
  required,
@@ -3063,60 +3269,7 @@ async function captureDOM(element, options) {
3063
3269
  b = Math.max(b, extY + Math.max(oy, 0));
3064
3270
  t = Math.max(t, extY + Math.max(-oy, 0));
3065
3271
  }
3066
- return {
3067
- bleed: { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b), left: Math.ceil(l) },
3068
- has: found
3069
- };
3070
- }
3071
- function parseBoxShadow(cs) {
3072
- const v = cs.boxShadow || "";
3073
- if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3074
- const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
3075
- let t = 0, r = 0, b2 = 0, l = 0;
3076
- for (const part of parts) {
3077
- const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
3078
- if (nums.length < 2) continue;
3079
- const [ox2, oy2, blur = 0, spread = 0] = nums;
3080
- const extX = Math.abs(ox2) + blur + spread;
3081
- const extY = Math.abs(oy2) + blur + spread;
3082
- r = Math.max(r, extX + Math.max(ox2, 0));
3083
- l = Math.max(l, extX + Math.max(-ox2, 0));
3084
- b2 = Math.max(b2, extY + Math.max(oy2, 0));
3085
- t = Math.max(t, extY + Math.max(-oy2, 0));
3086
- }
3087
- return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
3088
- }
3089
- function parseFilterBlur(cs) {
3090
- const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
3091
- const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
3092
- return { top: b2, right: b2, bottom: b2, left: b2 };
3093
- }
3094
- function parseOutline(cs) {
3095
- if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3096
- const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
3097
- return { top: w2, right: w2, bottom: w2, left: w2 };
3098
- }
3099
- function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
3100
- const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
3101
- function pt(x, y) {
3102
- let X = x - ox2, Y = y - oy2;
3103
- let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
3104
- X2 += ox2 + e2;
3105
- Y2 += oy2 + f2;
3106
- return [X2, Y2];
3107
- }
3108
- const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
3109
- let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
3110
- for (const [X, Y] of P) {
3111
- if (X < minX2) minX2 = X;
3112
- if (Y < minY2) minY2 = Y;
3113
- if (X > maxX2) maxX2 = X;
3114
- if (Y > maxY2) maxY2 = Y;
3115
- }
3116
- return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
3117
- }
3118
- function hasTFBBox(el) {
3119
- return hasBBoxAffectingTransform(el);
3272
+ return { bleed: { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b), left: Math.ceil(l) }, has: found };
3120
3273
  }
3121
3274
  const rect = element.getBoundingClientRect();
3122
3275
  const w0 = Math.max(1, Math.ceil(element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
@@ -3145,28 +3298,38 @@ async function captureDOM(element, options) {
3145
3298
  h = h0;
3146
3299
  }
3147
3300
  let minX = 0, minY = 0, maxX = w0, maxY = h0;
3148
- if (hasTFBBox(element)) {
3149
- const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3150
- const ind2 = readIndividualTransforms(element);
3151
- const TOTAL = readTotalTransformMatrix({
3152
- baseTransform: baseTransform2,
3153
- rotate: ind2.rotate || "0deg",
3154
- scale: ind2.scale,
3155
- translate: ind2.translate
3156
- });
3157
- const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
3158
- const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
3159
- const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
3160
- minX = bb.minX;
3161
- minY = bb.minY;
3162
- maxX = bb.maxX;
3163
- maxY = bb.maxY;
3301
+ if (straighten && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3302
+ const M2 = { a: rootTransform2D.a, b: rootTransform2D.b || 0, c: rootTransform2D.c || 0, d: rootTransform2D.d || 1, e: 0, f: 0 };
3303
+ const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
3304
+ minX = bb2.minX;
3305
+ minY = bb2.minY;
3306
+ maxX = bb2.maxX;
3307
+ maxY = bb2.maxY;
3308
+ } else {
3309
+ const useTFBBox = !straighten && hasTFBBox(element);
3310
+ if (useTFBBox) {
3311
+ const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3312
+ const ind2 = readIndividualTransforms(element);
3313
+ const TOTAL = readTotalTransformMatrix({
3314
+ baseTransform: baseTransform2,
3315
+ rotate: ind2.rotate || "0deg",
3316
+ scale: ind2.scale,
3317
+ translate: ind2.translate
3318
+ });
3319
+ const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
3320
+ const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
3321
+ const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
3322
+ minX = bb.minX;
3323
+ minY = bb.minY;
3324
+ maxX = bb.maxX;
3325
+ maxY = bb.maxY;
3326
+ }
3164
3327
  }
3165
3328
  const bleedShadow = parseBoxShadow(csEl);
3166
3329
  const bleedBlur = parseFilterBlur(csEl);
3167
3330
  const bleedOutline = parseOutline(csEl);
3168
3331
  const drop = parseFilterDropShadows(csEl);
3169
- const bleed = {
3332
+ const bleed = noShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3170
3333
  top: bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top,
3171
3334
  right: bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right,
3172
3335
  bottom: bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom,
@@ -3181,7 +3344,9 @@ async function captureDOM(element, options) {
3181
3344
  const outW = Math.max(1, Math.round(vbW0 * (hasW || hasH ? w / w0 : 1)));
3182
3345
  const outH = Math.max(1, Math.round(vbH0 * (hasH || hasW ? h / h0 : 1)));
3183
3346
  const svgNS = "http://www.w3.org/2000/svg";
3184
- const pad = isSafari() ? 1 : 0;
3347
+ const basePad = isSafari() ? 1 : 0;
3348
+ const extraPad = straighten ? 1 : 0;
3349
+ const pad = basePad + extraPad;
3185
3350
  const fo = document.createElementNS(svgNS, "foreignObject");
3186
3351
  const vbMinX = Math.floor(minX);
3187
3352
  const vbMinY = Math.floor(minY);
@@ -3206,20 +3371,7 @@ async function captureDOM(element, options) {
3206
3371
  const vbW = vbW0 + pad * 2;
3207
3372
  const vbH = vbH0 + pad * 2;
3208
3373
  const wantsSize = hasW || hasH;
3209
- options.meta = {
3210
- w0,
3211
- // ancho natural del elemento
3212
- h0,
3213
- // alto natural
3214
- vbW,
3215
- // ancho del viewBox
3216
- vbH,
3217
- // alto del viewBox
3218
- targetW: w,
3219
- // ancho deseado según options.width
3220
- targetH: h
3221
- // alto deseado según options.height
3222
- };
3374
+ options.meta = { w0, h0, vbW, vbH, targetW: w, targetH: h };
3223
3375
  const svgOutW = isSafari() && wantsSize ? vbW : outW + pad * 2;
3224
3376
  const svgOutH = isSafari() && wantsSize ? vbH : outH + pad * 2;
3225
3377
  const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
@@ -3233,6 +3385,115 @@ async function captureDOM(element, options) {
3233
3385
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3234
3386
  return dataURL;
3235
3387
  }
3388
+ function normalizeRootTransforms(originalEl, cloneRoot) {
3389
+ if (!originalEl || !cloneRoot || !cloneRoot.style) return null;
3390
+ const cs = getComputedStyle(originalEl);
3391
+ try {
3392
+ cloneRoot.style.transformOrigin = "0 0";
3393
+ } catch {
3394
+ }
3395
+ try {
3396
+ if ("translate" in cloneRoot.style) cloneRoot.style.translate = "none";
3397
+ if ("rotate" in cloneRoot.style) cloneRoot.style.rotate = "none";
3398
+ } catch {
3399
+ }
3400
+ const tr = cs.transform || "none";
3401
+ if (!tr || tr === "none") {
3402
+ try {
3403
+ const M = matrixFromComputed(originalEl);
3404
+ if (M.a === 1 && M.b === 0 && M.c === 0 && M.d === 1) {
3405
+ cloneRoot.style.transform = "none";
3406
+ return { a: 1, b: 0, c: 0, d: 1 };
3407
+ }
3408
+ } catch {
3409
+ }
3410
+ }
3411
+ const m2d = tr.match(/^matrix\(\s*([^)]+)\)$/i);
3412
+ if (m2d) {
3413
+ const nums = m2d[1].split(",").map((v) => parseFloat(v.trim()));
3414
+ if (nums.length === 6 && nums.every(Number.isFinite)) {
3415
+ const [a, b, c, d] = nums;
3416
+ const scaleX = Math.sqrt(a * a + b * b) || 0;
3417
+ let a1 = 0, b1 = 0, shear = 0, c2 = 0, d2 = 0, scaleY = 0;
3418
+ if (scaleX > 0) {
3419
+ a1 = a / scaleX;
3420
+ b1 = b / scaleX;
3421
+ shear = a1 * c + b1 * d;
3422
+ c2 = c - a1 * shear;
3423
+ d2 = d - b1 * shear;
3424
+ scaleY = Math.sqrt(c2 * c2 + d2 * d2) || 0;
3425
+ if (scaleY > 0) shear = shear / scaleY;
3426
+ else shear = 0;
3427
+ }
3428
+ const aP = scaleX;
3429
+ const bP = 0;
3430
+ const cP = shear * scaleY;
3431
+ const dP = scaleY;
3432
+ try {
3433
+ cloneRoot.style.transform = `matrix(${aP}, ${bP}, ${cP}, ${dP}, 0, 0)`;
3434
+ } catch {
3435
+ }
3436
+ return { a: aP, b: bP, c: cP, d: dP };
3437
+ }
3438
+ }
3439
+ try {
3440
+ const legacy = String(tr).trim();
3441
+ cloneRoot.style.transform = legacy + " translate(0px, 0px) rotate(0deg)";
3442
+ return null;
3443
+ } catch {
3444
+ return null;
3445
+ }
3446
+ }
3447
+ function parseBoxShadow(cs) {
3448
+ const v = cs.boxShadow || "";
3449
+ if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3450
+ const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
3451
+ let t = 0, r = 0, b2 = 0, l = 0;
3452
+ for (const part of parts) {
3453
+ const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
3454
+ if (nums.length < 2) continue;
3455
+ const [ox2, oy2, blur = 0, spread = 0] = nums;
3456
+ const extX = Math.abs(ox2) + blur + spread;
3457
+ const extY = Math.abs(oy2) + blur + spread;
3458
+ r = Math.max(r, extX + Math.max(ox2, 0));
3459
+ l = Math.max(l, extX + Math.max(-ox2, 0));
3460
+ b2 = Math.max(b2, extY + Math.max(oy2, 0));
3461
+ t = Math.max(t, extY + Math.max(-oy2, 0));
3462
+ }
3463
+ return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
3464
+ }
3465
+ function parseFilterBlur(cs) {
3466
+ const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
3467
+ const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
3468
+ return { top: b2, right: b2, bottom: b2, left: b2 };
3469
+ }
3470
+ function parseOutline(cs) {
3471
+ if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
3472
+ const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
3473
+ return { top: w2, right: w2, bottom: w2, left: w2 };
3474
+ }
3475
+ function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
3476
+ const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
3477
+ function pt(x, y) {
3478
+ let X = x - ox2, Y = y - oy2;
3479
+ let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
3480
+ X2 += ox2 + e2;
3481
+ Y2 += oy2 + f2;
3482
+ return [X2, Y2];
3483
+ }
3484
+ const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
3485
+ let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
3486
+ for (const [X, Y] of P) {
3487
+ if (X < minX2) minX2 = X;
3488
+ if (Y < minY2) minY2 = Y;
3489
+ if (X > maxX2) maxX2 = X;
3490
+ if (Y > maxY2) maxY2 = Y;
3491
+ }
3492
+ return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
3493
+ }
3494
+ function hasTFBBox(el) {
3495
+ return hasBBoxAffectingTransform(el);
3496
+ }
3236
3497
  function matrixFromComputed(el) {
3237
3498
  const tr = getComputedStyle(el).transform;
3238
3499
  if (!tr || tr === "none") return new DOMMatrix();
@@ -3385,20 +3646,20 @@ function createContext(options = {}) {
3385
3646
  excludeMode: options.excludeMode ?? "hide",
3386
3647
  filter: options.filter ?? null,
3387
3648
  filterMode: options.filterMode ?? "hide",
3649
+ // Placeholders
3388
3650
  placeholders: options.placeholders !== false,
3389
3651
  // default true
3390
- // Fuentes
3652
+ // Fonts
3391
3653
  embedFonts: options.embedFonts ?? false,
3392
3654
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
3393
3655
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
3394
3656
  excludeFonts: options.excludeFonts ?? void 0,
3395
3657
  fallbackURL: options.fallbackURL ?? void 0,
3396
- /** Cache policy (disabled|full|auto|soft). Default: "soft" */
3397
3658
  /** @type {CachePolicy} */
3398
3659
  cache: cachePolicy,
3399
- // Red
3660
+ // Network
3400
3661
  useProxy: typeof options.useProxy === "string" ? options.useProxy : "",
3401
- // Salida
3662
+ // Output
3402
3663
  width: options.width ?? null,
3403
3664
  height: options.height ?? null,
3404
3665
  format: resolvedFormat,
@@ -3406,8 +3667,11 @@ function createContext(options = {}) {
3406
3667
  quality: options.quality ?? 0.92,
3407
3668
  dpr: options.dpr ?? (window.devicePixelRatio || 1),
3408
3669
  backgroundColor: options.backgroundColor ?? (["jpg", "jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
3409
- filename: options.filename ?? "snapDOM"
3410
- // Plugins (reservado para futura activación)
3670
+ filename: options.filename ?? "snapDOM",
3671
+ // NEW flags (user-friendly)
3672
+ straighten: options.straighten ?? false,
3673
+ noShadows: options.noShadows ?? false
3674
+ // Plugins (reservado)
3411
3675
  // plugins: normalizePlugins(...),
3412
3676
  };
3413
3677
  }
@@ -3597,9 +3861,20 @@ async function toImg(url, options) {
3597
3861
  const k = height / Math.max(1, refH);
3598
3862
  img.style.height = `${height}px`;
3599
3863
  img.style.width = `${Math.round(refW * k)}px`;
3600
- } else if (scale !== 1) {
3601
- img.style.width = `${Math.round(img.naturalWidth * scale)}px`;
3602
- img.style.height = `${Math.round(img.naturalHeight * scale)}px`;
3864
+ } else {
3865
+ const cssW = Math.round(img.naturalWidth * scale);
3866
+ const cssH = Math.round(img.naturalHeight * scale);
3867
+ img.style.width = `${cssW}px`;
3868
+ img.style.height = `${cssH}px`;
3869
+ if (typeof url === "string" && url.startsWith("data:image/svg+xml")) {
3870
+ try {
3871
+ const decoded = decodeURIComponent(url.split(",")[1]);
3872
+ const patched = decoded.replace(/width="[^"]*"/, `width="${cssW}"`).replace(/height="[^"]*"/, `height="${cssH}"`);
3873
+ url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(patched)}`;
3874
+ img.src = url;
3875
+ } catch {
3876
+ }
3877
+ }
3603
3878
  }
3604
3879
  return img;
3605
3880
  }
@@ -3655,7 +3930,7 @@ async function snapdom(element, userOptions) {
3655
3930
  await safariWarmup(element, userOptions);
3656
3931
  console.log("Iteraci\xF3n n\xFAmero:", i);
3657
3932
  _safariWarmup = false;
3658
- } catch (error) {
3933
+ } catch {
3659
3934
  }
3660
3935
  }
3661
3936
  }
@@ -3663,7 +3938,7 @@ async function snapdom(element, userOptions) {
3663
3938
  if (!context.snap) {
3664
3939
  context.snap = {
3665
3940
  toPng: (el, opts) => snapdom.toPng(el, opts),
3666
- toImg: (el, opts) => snapdom.toImg(el, opts)
3941
+ toSvg: (el, opts) => snapdom.toSvg(el, opts)
3667
3942
  };
3668
3943
  }
3669
3944
  return snapdom.capture(element, context, INTERNAL_TOKEN);
@@ -3685,6 +3960,7 @@ snapdom.capture = async (el, context, _token) => {
3685
3960
  url,
3686
3961
  toRaw: () => url,
3687
3962
  toImg: (opts) => toImg(url, ensureContext(opts)),
3963
+ toSvg: (opts) => toImg(url, ensureContext(opts)),
3688
3964
  toCanvas: (opts) => toCanvas(url, ensureContext(opts)),
3689
3965
  toBlob: (opts) => toBlob(url, ensureContext(opts)),
3690
3966
  toPng: withFormat("png"),
@@ -3695,6 +3971,7 @@ snapdom.capture = async (el, context, _token) => {
3695
3971
  };
3696
3972
  snapdom.toRaw = (el, options) => snapdom(el, options).then((result) => result.toRaw());
3697
3973
  snapdom.toImg = (el, options) => snapdom(el, options).then((result) => result.toImg());
3974
+ snapdom.toSvg = (el, options) => snapdom(el, options).then((result) => result.toSvg());
3698
3975
  snapdom.toCanvas = (el, options) => snapdom(el, options).then((result) => result.toCanvas());
3699
3976
  snapdom.toBlob = (el, options) => snapdom(el, options).then((result) => result.toBlob());
3700
3977
  snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then((result) => result.toPng());
@@ -3833,7 +4110,7 @@ async function preCache(root = document, options = {}) {
3833
4110
  const families = new Set(
3834
4111
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
3835
4112
  );
3836
- await ensureFontsReady(families, 2);
4113
+ await ensureFontsReady(families, 3);
3837
4114
  }
3838
4115
  await embedCustomFonts({
3839
4116
  required,