@zumer/snapdom 1.9.14 → 2.0.0-dev.1

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.14
4
+ * v.2.0.0-dev.1
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -23,6 +23,7 @@ var cache = {
23
23
  }
24
24
  };
25
25
  function applyCachePolicy(policy = "soft") {
26
+ cache.session.__counterEpoch = (cache.session.__counterEpoch || 0) + 1;
26
27
  switch (policy) {
27
28
  case "auto": {
28
29
  cache.session.styleMap = /* @__PURE__ */ new Map();
@@ -808,6 +809,27 @@ function stripHeightForWrappers(el, cs, snap) {
808
809
  }
809
810
 
810
811
  // src/modules/CSSVar.js
812
+ var KEY_PROPS = ["fill", "stroke", "color", "background-color", "stop-color"];
813
+ var __BASELINE_CACHE = /* @__PURE__ */ new Map();
814
+ function getBaselineComputed(tagName, ns) {
815
+ const key = ns + "::" + tagName.toLowerCase();
816
+ let entry = __BASELINE_CACHE.get(key);
817
+ if (entry) return entry;
818
+ const doc = document;
819
+ const el = ns === "http://www.w3.org/2000/svg" ? doc.createElementNS(ns, tagName) : doc.createElement(tagName);
820
+ const holder = doc.createElement("div");
821
+ holder.style.cssText = "position:absolute;left:-99999px;top:-99999px;contain:strict;display:block;";
822
+ holder.appendChild(el);
823
+ doc.documentElement.appendChild(holder);
824
+ const cs = getComputedStyle(el);
825
+ const base = {};
826
+ for (const p of KEY_PROPS) {
827
+ base[p] = cs.getPropertyValue(p) || "";
828
+ }
829
+ holder.remove();
830
+ __BASELINE_CACHE.set(key, base);
831
+ return base;
832
+ }
811
833
  function resolveCSSVars(sourceEl, cloneEl) {
812
834
  if (!(sourceEl instanceof Element) || !(cloneEl instanceof Element)) return;
813
835
  const styleAttr = sourceEl.getAttribute?.("style");
@@ -822,42 +844,62 @@ function resolveCSSVars(sourceEl, cloneEl) {
822
844
  }
823
845
  }
824
846
  }
825
- if (!hasVar) return;
826
- let cs;
827
- try {
828
- cs = getComputedStyle(sourceEl);
829
- } catch {
830
- return;
847
+ let cs = null;
848
+ if (hasVar) {
849
+ try {
850
+ cs = getComputedStyle(sourceEl);
851
+ } catch {
852
+ }
831
853
  }
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 {
854
+ if (hasVar) {
855
+ const author = sourceEl.style;
856
+ if (author && author.length) {
857
+ for (let i = 0; i < author.length; i++) {
858
+ const prop = author[i];
859
+ const val = author.getPropertyValue(prop);
860
+ if (!val || !val.includes("var(")) continue;
861
+ const resolved = cs && cs.getPropertyValue(prop);
862
+ if (resolved) {
863
+ try {
864
+ cloneEl.style.setProperty(prop, resolved.trim(), author.getPropertyPriority(prop));
865
+ } catch {
866
+ }
843
867
  }
844
868
  }
845
869
  }
846
870
  }
847
- if (sourceEl.attributes?.length) {
871
+ if (hasVar && sourceEl.attributes?.length) {
848
872
  const attrs = sourceEl.attributes;
849
873
  for (let i = 0; i < attrs.length; i++) {
850
874
  const a = attrs[i];
851
875
  if (!a || typeof a.value !== "string" || !a.value.includes("var(")) continue;
852
876
  const propName = a.name;
853
- let resolved = "";
877
+ const resolved = cs && cs.getPropertyValue(propName);
878
+ if (resolved) {
879
+ try {
880
+ cloneEl.style.setProperty(propName, resolved.trim());
881
+ } catch {
882
+ }
883
+ }
884
+ }
885
+ }
886
+ if (!hasVar) {
887
+ if (!cs) {
854
888
  try {
855
- resolved = cs.getPropertyValue(propName);
889
+ cs = getComputedStyle(sourceEl);
856
890
  } catch {
891
+ cs = null;
857
892
  }
858
- if (resolved) {
893
+ }
894
+ if (!cs) return;
895
+ const ns = sourceEl.namespaceURI || "html";
896
+ const base = getBaselineComputed(sourceEl.tagName, ns);
897
+ for (const prop of KEY_PROPS) {
898
+ const v = cs.getPropertyValue(prop) || "";
899
+ const b = base[prop] || "";
900
+ if (v && v !== b) {
859
901
  try {
860
- cloneEl.style.setProperty(propName, resolved.trim());
902
+ cloneEl.style.setProperty(prop, v.trim());
861
903
  } catch {
862
904
  }
863
905
  }
@@ -1180,11 +1222,50 @@ async function deepClone(node, sessionCache, options) {
1180
1222
  return clone2;
1181
1223
  }
1182
1224
  if (node.tagName === "CANVAS") {
1183
- const dataURL = node.toDataURL();
1225
+ let url = "";
1226
+ try {
1227
+ const ctx = node.getContext("2d", { willReadFrequently: true });
1228
+ try {
1229
+ ctx && ctx.getImageData(0, 0, 1, 1);
1230
+ } catch {
1231
+ }
1232
+ await new Promise((r) => requestAnimationFrame(r));
1233
+ url = node.toDataURL("image/png");
1234
+ if (!url || url === "data:,") {
1235
+ try {
1236
+ ctx && ctx.getImageData(0, 0, 1, 1);
1237
+ } catch {
1238
+ }
1239
+ await new Promise((r) => requestAnimationFrame(r));
1240
+ url = node.toDataURL("image/png");
1241
+ if (!url || url === "data:,") {
1242
+ const scratch = document.createElement("canvas");
1243
+ scratch.width = node.width;
1244
+ scratch.height = node.height;
1245
+ const sctx = scratch.getContext("2d");
1246
+ if (sctx) {
1247
+ sctx.drawImage(node, 0, 0);
1248
+ url = scratch.toDataURL("image/png");
1249
+ }
1250
+ }
1251
+ }
1252
+ } catch {
1253
+ }
1184
1254
  const img = document.createElement("img");
1185
- img.src = dataURL;
1255
+ try {
1256
+ img.decoding = "sync";
1257
+ img.loading = "eager";
1258
+ } catch {
1259
+ }
1260
+ if (url) img.src = url;
1186
1261
  img.width = node.width;
1187
1262
  img.height = node.height;
1263
+ try {
1264
+ const cs = getComputedStyle(node);
1265
+ if (cs.width) img.style.width = cs.width;
1266
+ if (cs.height) img.style.height = cs.height;
1267
+ } catch {
1268
+ }
1188
1269
  sessionCache.nodeMap.set(img, node);
1189
1270
  inlineAllStyles(node, img, sessionCache, options);
1190
1271
  return img;
@@ -1362,6 +1443,123 @@ function isIconFont2(input) {
1362
1443
  if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
1363
1444
  return false;
1364
1445
  }
1446
+ function isMaterialFamily(family = "") {
1447
+ const s = String(family).toLowerCase();
1448
+ return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
1449
+ }
1450
+ async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1451
+ try {
1452
+ await Promise.all([
1453
+ document.fonts.load(`400 ${px}px "${family.replace(/["']/g, "")}"`),
1454
+ document.fonts.ready
1455
+ ]);
1456
+ } catch {
1457
+ }
1458
+ }
1459
+ function resolvePaintColor(cs) {
1460
+ 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
+ }
1465
+ const c = cs.color?.trim();
1466
+ return c && c !== "inherit" ? c : "#000";
1467
+ }
1468
+ async function materialIconToImage(ligatureText, {
1469
+ family = "Material Icons",
1470
+ weight = "normal",
1471
+ fontSize = 32,
1472
+ color = "#000",
1473
+ variation = ""
1474
+ } = {}) {
1475
+ const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
1476
+ const dpr = window.devicePixelRatio || 1;
1477
+ await ensureMaterialFontsReady(fam, fontSize);
1478
+ const span = document.createElement("span");
1479
+ span.textContent = ligatureText;
1480
+ span.style.position = "absolute";
1481
+ span.style.visibility = "hidden";
1482
+ span.style.left = "-99999px";
1483
+ span.style.whiteSpace = "nowrap";
1484
+ span.style.fontFamily = `"${fam}"`;
1485
+ span.style.fontWeight = String(weight || "normal");
1486
+ span.style.fontSize = `${fontSize}px`;
1487
+ span.style.lineHeight = "1";
1488
+ span.style.margin = "0";
1489
+ span.style.padding = "0";
1490
+ span.style.fontFeatureSettings = "'liga' 1";
1491
+ span.style.fontVariantLigatures = "normal";
1492
+ if (variation) span.style.fontVariationSettings = variation;
1493
+ span.style.color = color;
1494
+ document.body.appendChild(span);
1495
+ const rect = span.getBoundingClientRect();
1496
+ const width = Math.max(1, Math.ceil(rect.width));
1497
+ const height = Math.max(1, Math.ceil(rect.height));
1498
+ document.body.removeChild(span);
1499
+ const canvas = document.createElement("canvas");
1500
+ canvas.width = width * dpr;
1501
+ canvas.height = height * dpr;
1502
+ const ctx = canvas.getContext("2d");
1503
+ ctx.scale(dpr, dpr);
1504
+ ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px "${fam}"`;
1505
+ ctx.textAlign = "left";
1506
+ ctx.textBaseline = "top";
1507
+ ctx.fillStyle = color;
1508
+ try {
1509
+ ctx.fontKerning = "normal";
1510
+ } catch {
1511
+ }
1512
+ ctx.fillText(ligatureText, 0, 0);
1513
+ return {
1514
+ dataUrl: canvas.toDataURL(),
1515
+ width,
1516
+ height
1517
+ };
1518
+ }
1519
+ async function ligatureIconToImage(cloneRoot, sourceRoot) {
1520
+ if (!(cloneRoot instanceof Element)) return 0;
1521
+ const selector = '.material-icons, [class*="material-symbols"]';
1522
+ const cloneNodes = Array.from(
1523
+ cloneRoot.querySelectorAll(selector)
1524
+ ).filter((n) => n && n.textContent && n.textContent.trim());
1525
+ if (cloneNodes.length === 0) return 0;
1526
+ const sourceNodes = sourceRoot instanceof Element ? Array.from(sourceRoot.querySelectorAll(selector)).filter((n) => n && n.textContent && n.textContent.trim()) : [];
1527
+ let replaced = 0;
1528
+ for (let i = 0; i < cloneNodes.length; i++) {
1529
+ const el = cloneNodes[i];
1530
+ const src = sourceNodes[i] || null;
1531
+ try {
1532
+ const csSrc = src ? getComputedStyle(src) : getComputedStyle(el);
1533
+ const family = csSrc.fontFamily || "Material Icons";
1534
+ if (!isMaterialFamily(family)) continue;
1535
+ const text = (src || el).textContent.trim();
1536
+ 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 : "";
1541
+ const { dataUrl, width, height } = await materialIconToImage(text, {
1542
+ family,
1543
+ weight,
1544
+ fontSize: size,
1545
+ color,
1546
+ variation
1547
+ });
1548
+ el.textContent = "";
1549
+ const img = el.ownerDocument.createElement("img");
1550
+ img.src = dataUrl;
1551
+ img.alt = text;
1552
+ img.style.height = `${size}px`;
1553
+ img.style.width = `${Math.max(1, Math.round(width / height * size))}px`;
1554
+ img.style.objectFit = "contain";
1555
+ img.style.verticalAlign = getComputedStyle(el).verticalAlign || "baseline";
1556
+ el.appendChild(img);
1557
+ replaced++;
1558
+ } catch {
1559
+ }
1560
+ }
1561
+ return replaced;
1562
+ }
1365
1563
 
1366
1564
  // src/modules/fonts.js
1367
1565
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
@@ -2176,6 +2374,8 @@ function formatCounter(value, style) {
2176
2374
  }
2177
2375
  }
2178
2376
  function buildCounterContext(root) {
2377
+ const getEpoch = () => cache?.session?.__counterEpoch ?? 0;
2378
+ let run = getEpoch();
2179
2379
  const nodeCounters = /* @__PURE__ */ new WeakMap();
2180
2380
  const rootEl = root instanceof Document ? root.documentElement : root;
2181
2381
  const isLi = (el) => el && el.tagName === "LI";
@@ -2268,6 +2468,14 @@ function buildCounterContext(root) {
2268
2468
  };
2269
2469
  const empty = /* @__PURE__ */ new Map();
2270
2470
  build(rootEl, empty, empty);
2471
+ function ensureFresh() {
2472
+ const now = getEpoch();
2473
+ if (now !== run) {
2474
+ run = now;
2475
+ const empty2 = /* @__PURE__ */ new Map();
2476
+ build(rootEl, empty2, empty2);
2477
+ }
2478
+ }
2271
2479
  return {
2272
2480
  /**
2273
2481
  * Get top value for counter name at given node.
@@ -2275,6 +2483,7 @@ function buildCounterContext(root) {
2275
2483
  * @param {string} name
2276
2484
  */
2277
2485
  get(node, name) {
2486
+ ensureFresh();
2278
2487
  const s = nodeCounters.get(node)?.get(name);
2279
2488
  return s && s.length ? s[s.length - 1] : 0;
2280
2489
  },
@@ -2284,6 +2493,7 @@ function buildCounterContext(root) {
2284
2493
  * @param {string} name
2285
2494
  */
2286
2495
  getStack(node, name) {
2496
+ ensureFresh();
2287
2497
  const s = nodeCounters.get(node)?.get(name);
2288
2498
  return s ? s.slice() : [];
2289
2499
  }
@@ -2317,8 +2527,8 @@ function resolveCountersInContent(raw, node, ctx) {
2317
2527
  }
2318
2528
 
2319
2529
  // src/modules/pseudo.js
2320
- var counterCtx = null;
2321
2530
  var __siblingCounters = /* @__PURE__ */ new WeakMap();
2531
+ var __pseudoEpoch = -1;
2322
2532
  function unquoteDoubleStrings2(s) {
2323
2533
  return (s || "").replace(/"([^"]*)"/g, "$1");
2324
2534
  }
@@ -2415,12 +2625,19 @@ function resolvePseudoContentAndIncs(node, pseudo, baseCtx) {
2415
2625
  }
2416
2626
  async function inlinePseudoElements(source, clone, sessionCache, options) {
2417
2627
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
2418
- if (!counterCtx) {
2628
+ const epoch = cache?.session?.__counterEpoch ?? 0;
2629
+ if (__pseudoEpoch !== epoch) {
2630
+ __siblingCounters = /* @__PURE__ */ new WeakMap();
2631
+ if (sessionCache) sessionCache.__counterCtx = null;
2632
+ __pseudoEpoch = epoch;
2633
+ }
2634
+ if (!sessionCache.__counterCtx) {
2419
2635
  try {
2420
- counterCtx = buildCounterContext(source.ownerDocument || document);
2636
+ sessionCache.__counterCtx = buildCounterContext(source.ownerDocument || document);
2421
2637
  } catch {
2422
2638
  }
2423
2639
  }
2640
+ const counterCtx = sessionCache.__counterCtx;
2424
2641
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
2425
2642
  try {
2426
2643
  const style = getStyle(source, pseudo);
@@ -2485,7 +2702,8 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2485
2702
  }
2486
2703
  const pseudoEl = document.createElement("span");
2487
2704
  pseudoEl.dataset.snapdomPseudo = pseudo;
2488
- pseudoEl.style.verticalAlign = "middle";
2705
+ pseudoEl.style.display = "inline";
2706
+ pseudoEl.style.verticalAlign = "baseline";
2489
2707
  pseudoEl.style.pointerEvents = "none";
2490
2708
  const snapshot = snapshotComputedStyle(style);
2491
2709
  const key = getStyleKey(snapshot, "span");
@@ -2513,9 +2731,23 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2513
2731
  } else if (!isIconFont22 && hasExplicitContent) {
2514
2732
  pseudoEl.textContent = cleanContent;
2515
2733
  }
2516
- pseudoEl.style.background = "none";
2517
- if ("mask" in pseudoEl.style) {
2518
- pseudoEl.style.mask = "none";
2734
+ pseudoEl.style.backgroundImage = "none";
2735
+ if ("maskImage" in pseudoEl.style) pseudoEl.style.maskImage = "none";
2736
+ if ("webkitMaskImage" in pseudoEl.style) pseudoEl.style.webkitMaskImage = "none";
2737
+ try {
2738
+ pseudoEl.style.backgroundRepeat = style.backgroundRepeat;
2739
+ pseudoEl.style.backgroundSize = style.backgroundSize;
2740
+ if (style.backgroundPositionX && style.backgroundPositionY) {
2741
+ pseudoEl.style.backgroundPositionX = style.backgroundPositionX;
2742
+ pseudoEl.style.backgroundPositionY = style.backgroundPositionY;
2743
+ } else {
2744
+ pseudoEl.style.backgroundPosition = style.backgroundPosition;
2745
+ }
2746
+ pseudoEl.style.backgroundOrigin = style.backgroundOrigin;
2747
+ pseudoEl.style.backgroundClip = style.backgroundClip;
2748
+ pseudoEl.style.backgroundAttachment = style.backgroundAttachment;
2749
+ pseudoEl.style.backgroundBlendMode = style.backgroundBlendMode;
2750
+ } catch {
2519
2751
  }
2520
2752
  if (hasBg) {
2521
2753
  try {
@@ -2558,53 +2790,113 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2558
2790
  }
2559
2791
 
2560
2792
  // src/modules/svgDefs.js
2561
- function inlineExternalDefsAndSymbols(rootElement) {
2562
- if (!rootElement) return;
2563
- const usedIds = /* @__PURE__ */ new Set();
2564
- rootElement.querySelectorAll("use").forEach((use) => {
2565
- const href = use.getAttribute("xlink:href") || use.getAttribute("href");
2566
- if (href && href.startsWith("#")) {
2567
- usedIds.add(href.slice(1));
2793
+ function inlineExternalDefsAndSymbols(element, lookupRoot) {
2794
+ if (!element || !(element instanceof Element)) return;
2795
+ const doc = element.ownerDocument || document;
2796
+ const searchRoot = lookupRoot || doc;
2797
+ const svgRoots = element instanceof SVGSVGElement ? [element] : Array.from(element.querySelectorAll("svg"));
2798
+ if (svgRoots.length === 0) return;
2799
+ const URL_ID_RE = /url\(\s*#([^)]+)\)/g;
2800
+ const URL_ATTRS = [
2801
+ "fill",
2802
+ "stroke",
2803
+ "filter",
2804
+ "clip-path",
2805
+ "mask",
2806
+ "marker",
2807
+ "marker-start",
2808
+ "marker-mid",
2809
+ "marker-end"
2810
+ ];
2811
+ const cssEscape = (s) => window.CSS && CSS.escape ? CSS.escape(s) : s.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
2812
+ const globalExistingIds = new Set(
2813
+ Array.from(element.querySelectorAll("[id]")).map((n) => n.id)
2814
+ );
2815
+ const neededIds = /* @__PURE__ */ new Set();
2816
+ let sawAnyReference = false;
2817
+ const addUrlIdsFromValue = (val) => {
2818
+ if (!val) return;
2819
+ URL_ID_RE.lastIndex = 0;
2820
+ let m;
2821
+ while (m = URL_ID_RE.exec(val)) {
2822
+ sawAnyReference = true;
2823
+ const id = (m[1] || "").trim();
2824
+ if (id && !globalExistingIds.has(id)) neededIds.add(id);
2568
2825
  }
2569
- });
2570
- if (!usedIds.size) return;
2571
- const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
2572
- const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
2573
- const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
2574
- let container = rootElement.querySelector("svg.inline-defs-container");
2575
- if (!container) {
2576
- container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2577
- container.setAttribute("aria-hidden", "true");
2578
- container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
2579
- container.classList.add("inline-defs-container");
2580
- rootElement.insertBefore(container, rootElement.firstChild);
2581
- }
2582
- const existingIds = /* @__PURE__ */ new Set();
2583
- rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
2584
- existingIds.add(el.id);
2585
- });
2586
- usedIds.forEach((id) => {
2587
- if (existingIds.has(id)) return;
2588
- const symbol = globalSymbols.find((sym) => sym.id === id);
2589
- if (symbol) {
2590
- container.appendChild(symbol.cloneNode(true));
2591
- existingIds.add(id);
2592
- return;
2826
+ };
2827
+ const collectReferencesInSvg = (rootSvg) => {
2828
+ const uses = rootSvg.querySelectorAll('use[href^="#"], use[xlink\\:href^="#"]');
2829
+ for (const u of uses) {
2830
+ const href = u.getAttribute("href") || u.getAttribute("xlink:href");
2831
+ if (!href || !href.startsWith("#")) continue;
2832
+ sawAnyReference = true;
2833
+ const id = href.slice(1).trim();
2834
+ if (id && !globalExistingIds.has(id)) neededIds.add(id);
2593
2835
  }
2594
- for (const defs of globalDefs) {
2595
- const defEl = defs.querySelector(`#${CSS.escape(id)}`);
2596
- if (defEl) {
2597
- let defsContainer = container.querySelector("defs");
2598
- if (!defsContainer) {
2599
- defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
2600
- container.appendChild(defsContainer);
2601
- }
2602
- defsContainer.appendChild(defEl.cloneNode(true));
2603
- existingIds.add(id);
2604
- break;
2836
+ let query = '*[style*="url("],*[fill^="url("], *[stroke^="url("],*[filter^="url("], *[clip-path^="url("],*[mask^="url("],*[marker^="url("], *[marker-start^="url("],*[marker-mid^="url("], *[marker-end^="url("]';
2837
+ const candidates = rootSvg.querySelectorAll(query);
2838
+ for (const el of candidates) {
2839
+ addUrlIdsFromValue(el.getAttribute("style") || "");
2840
+ for (const a of URL_ATTRS) addUrlIdsFromValue(el.getAttribute(a));
2841
+ }
2842
+ };
2843
+ for (const svg of svgRoots) collectReferencesInSvg(svg);
2844
+ if (!sawAnyReference) return;
2845
+ let defsHost = element.querySelector("svg.inline-defs-container");
2846
+ if (!defsHost) {
2847
+ defsHost = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
2848
+ defsHost.classList.add("inline-defs-container");
2849
+ defsHost.setAttribute("aria-hidden", "true");
2850
+ defsHost.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden");
2851
+ element.insertBefore(defsHost, element.firstChild || null);
2852
+ }
2853
+ let localDefs = defsHost.querySelector("defs") || null;
2854
+ const findGlobalById = (id) => {
2855
+ if (!id) return null;
2856
+ if (globalExistingIds.has(id)) return null;
2857
+ const esc = cssEscape(id);
2858
+ const tryFind = (sel) => {
2859
+ const el = searchRoot.querySelector(sel);
2860
+ return el && !element.contains(el) ? el : null;
2861
+ };
2862
+ return tryFind(`svg defs > *#${esc}`) || tryFind(`svg > symbol#${esc}`) || tryFind(`*#${esc}`);
2863
+ };
2864
+ if (!neededIds.size) return;
2865
+ const queued = new Set(neededIds);
2866
+ const inlined = /* @__PURE__ */ new Set();
2867
+ while (queued.size) {
2868
+ const id = queued.values().next().value;
2869
+ queued.delete(id);
2870
+ if (!id || globalExistingIds.has(id) || inlined.has(id)) continue;
2871
+ const source = findGlobalById(id);
2872
+ if (!source) {
2873
+ inlined.add(id);
2874
+ continue;
2875
+ }
2876
+ if (!localDefs) {
2877
+ localDefs = doc.createElementNS("http://www.w3.org/2000/svg", "defs");
2878
+ defsHost.appendChild(localDefs);
2879
+ }
2880
+ const clone = source.cloneNode(true);
2881
+ if (!clone.id) clone.setAttribute("id", id);
2882
+ localDefs.appendChild(clone);
2883
+ inlined.add(id);
2884
+ globalExistingIds.add(id);
2885
+ const walk = [clone, ...clone.querySelectorAll("*")];
2886
+ for (const node of walk) {
2887
+ const h = node.getAttribute?.("href") || node.getAttribute?.("xlink:href");
2888
+ if (h && h.startsWith("#")) {
2889
+ const ref = h.slice(1).trim();
2890
+ if (ref && !globalExistingIds.has(ref) && !inlined.has(ref)) queued.add(ref);
2891
+ }
2892
+ const style = node.getAttribute?.("style") || "";
2893
+ if (style) addUrlIdsFromValue(style);
2894
+ for (const a of URL_ATTRS) {
2895
+ const v = node.getAttribute?.(a);
2896
+ if (v) addUrlIdsFromValue(v);
2605
2897
  }
2606
2898
  }
2607
- });
2899
+ }
2608
2900
  }
2609
2901
 
2610
2902
  // src/modules/changeCSS.js
@@ -3050,6 +3342,17 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3050
3342
  "-webkit-mask-position-x",
3051
3343
  "-webkit-mask-position-y"
3052
3344
  ];
3345
+ const BG_LAYOUT_PROPS = [
3346
+ "background-position",
3347
+ "background-position-x",
3348
+ "background-position-y",
3349
+ "background-size",
3350
+ "background-repeat",
3351
+ "background-origin",
3352
+ "background-clip",
3353
+ "background-attachment",
3354
+ "background-blend-mode"
3355
+ ];
3053
3356
  const BORDER_AUX_PROPS = [
3054
3357
  "border-image-slice",
3055
3358
  "border-image-width",
@@ -3065,6 +3368,11 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3065
3368
  const bis = style.getPropertyValue("border-image-source");
3066
3369
  return bi && bi !== "none" || bis && bis !== "none";
3067
3370
  })();
3371
+ for (const prop of BG_LAYOUT_PROPS) {
3372
+ const v = style.getPropertyValue(prop);
3373
+ if (!v) continue;
3374
+ cloneNode.style.setProperty(prop, v);
3375
+ }
3068
3376
  for (const prop of URL_PROPS) {
3069
3377
  const val = style.getPropertyValue(prop);
3070
3378
  if (!val || val === "none") continue;
@@ -3089,7 +3397,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3089
3397
  }
3090
3398
  }
3091
3399
  const sChildren = Array.from(srcNode.children);
3092
- const cChildren = Array.from(cloneNode.children);
3400
+ const cChildren = Array.from(cloneNode.children).filter((el) => !(el.dataset && el.dataset.snapdomPseudo));
3093
3401
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
3094
3402
  queue.push([sChildren[i], cChildren[i]]);
3095
3403
  }
@@ -3153,6 +3461,81 @@ function isPlainTextContainer(el) {
3153
3461
  return Array.from(el.childNodes).some((n) => n.nodeType === Node.TEXT_NODE);
3154
3462
  }
3155
3463
 
3464
+ // src/core/plugins.js
3465
+ var __plugins = [];
3466
+ function normalizePlugin(spec) {
3467
+ if (!spec) return null;
3468
+ if (Array.isArray(spec)) {
3469
+ const [factory, options] = spec;
3470
+ return typeof factory === "function" ? factory(options) : factory;
3471
+ }
3472
+ if (typeof spec === "object" && "plugin" in spec) {
3473
+ const { plugin, options } = spec;
3474
+ return typeof plugin === "function" ? plugin(options) : plugin;
3475
+ }
3476
+ if (typeof spec === "function") return spec();
3477
+ return spec;
3478
+ }
3479
+ function registerPlugins(...defs) {
3480
+ const flat = defs.flat();
3481
+ for (const d of flat) {
3482
+ const inst = normalizePlugin(d);
3483
+ if (!inst) continue;
3484
+ if (!__plugins.some((p) => p && p.name && inst.name && p.name === inst.name)) {
3485
+ __plugins.push(inst);
3486
+ }
3487
+ }
3488
+ }
3489
+ function getContextPlugins(context) {
3490
+ const arr = context && Array.isArray(context.plugins) ? context.plugins : __plugins;
3491
+ return arr || __plugins;
3492
+ }
3493
+ async function runHook(name, context, payload) {
3494
+ let acc = payload;
3495
+ const list = getContextPlugins(context);
3496
+ for (const p of list) {
3497
+ const fn = p && typeof p[name] === "function" ? p[name] : null;
3498
+ if (!fn) continue;
3499
+ const out = await fn(context, acc);
3500
+ if (typeof out !== "undefined") acc = out;
3501
+ }
3502
+ return acc;
3503
+ }
3504
+ async function runAll(name, context, payload) {
3505
+ const outs = [];
3506
+ const list = getContextPlugins(context);
3507
+ for (const p of list) {
3508
+ const fn = p && typeof p[name] === "function" ? p[name] : null;
3509
+ if (!fn) continue;
3510
+ const out = await fn(context, payload);
3511
+ if (typeof out !== "undefined") outs.push(out);
3512
+ }
3513
+ return outs;
3514
+ }
3515
+ function mergePlugins(localDefs) {
3516
+ const out = [];
3517
+ if (Array.isArray(localDefs)) {
3518
+ for (const d of localDefs) {
3519
+ const inst = normalizePlugin(d);
3520
+ if (!inst || !inst.name) continue;
3521
+ const i = out.findIndex((x) => x && x.name === inst.name);
3522
+ if (i >= 0) out.splice(i, 1);
3523
+ out.push(inst);
3524
+ }
3525
+ }
3526
+ for (const g of __plugins) {
3527
+ if (g && g.name && !out.some((x) => x.name === g.name)) {
3528
+ out.push(g);
3529
+ }
3530
+ }
3531
+ return Object.freeze(out);
3532
+ }
3533
+ function attachSessionPlugins(context, localDefs, force = false) {
3534
+ if (!context || context.plugins && !force) return context;
3535
+ context.plugins = mergePlugins(localDefs);
3536
+ return context;
3537
+ }
3538
+
3156
3539
  // src/core/capture.js
3157
3540
  function stripRootShadows(originalEl, cloneRoot) {
3158
3541
  if (!originalEl || !cloneRoot || !cloneRoot.style) return;
@@ -3180,43 +3563,52 @@ async function captureDOM(element, options) {
3180
3563
  if (!element) throw new Error("Element cannot be null or undefined");
3181
3564
  applyCachePolicy(options.cache);
3182
3565
  const fast = options.fast;
3183
- const straighten = !!options.straighten;
3184
- const noShadows = !!options.noShadows;
3566
+ const outerTransforms = options.outerTransforms !== false;
3567
+ const outerShadows = !!options.outerShadows;
3568
+ let state = { element, options, plugins: options.plugins };
3185
3569
  let clone, classCSS, styleCache;
3186
3570
  let fontsCSS = "";
3187
3571
  let baseCSS = "";
3188
3572
  let dataURL;
3189
3573
  let svgString;
3190
3574
  let rootTransform2D = null;
3191
- const undoClamp = lineClamp(element);
3575
+ await runHook("beforeSnap", state);
3576
+ await runHook("beforeClone", state);
3577
+ const undoClamp = lineClamp(state.element);
3192
3578
  try {
3193
- ({ clone, classCSS, styleCache } = await prepareClone(element, options));
3194
- if (straighten && clone) {
3195
- rootTransform2D = normalizeRootTransforms(element, clone);
3579
+ ({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
3580
+ if (!outerTransforms && clone) {
3581
+ rootTransform2D = normalizeRootTransforms(state.element, clone);
3196
3582
  }
3197
- if (noShadows && clone) {
3198
- stripRootShadows(element, clone);
3583
+ if (!outerShadows && clone) {
3584
+ stripRootShadows(state.element, clone);
3199
3585
  }
3200
3586
  } finally {
3201
3587
  undoClamp();
3202
3588
  }
3589
+ state = { clone, classCSS, styleCache, ...state };
3590
+ await runHook("afterClone", state);
3591
+ try {
3592
+ await ligatureIconToImage(state.clone, state.element);
3593
+ } catch {
3594
+ }
3203
3595
  await new Promise((resolve) => {
3204
3596
  idle(async () => {
3205
- await inlineImages(clone, options);
3597
+ await inlineImages(state.clone, state.options);
3206
3598
  resolve();
3207
3599
  }, { fast });
3208
3600
  });
3209
3601
  await new Promise((resolve) => {
3210
3602
  idle(async () => {
3211
- await inlineBackgroundImages(element, clone, styleCache, options);
3603
+ await inlineBackgroundImages(state.element, state.clone, state.styleCache, state.options);
3212
3604
  resolve();
3213
3605
  }, { fast });
3214
3606
  });
3215
3607
  if (options.embedFonts) {
3216
3608
  await new Promise((resolve) => {
3217
3609
  idle(async () => {
3218
- const required = collectUsedFontVariants(element);
3219
- const usedCodepoints = collectUsedCodepoints(element);
3610
+ const required = collectUsedFontVariants(state.element);
3611
+ const usedCodepoints = collectUsedCodepoints(state.element);
3220
3612
  if (isSafari()) {
3221
3613
  const families = new Set(
3222
3614
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
@@ -3227,14 +3619,14 @@ async function captureDOM(element, options) {
3227
3619
  required,
3228
3620
  usedCodepoints,
3229
3621
  preCached: false,
3230
- exclude: options.excludeFonts,
3231
- useProxy: options.useProxy
3622
+ exclude: state.options.excludeFonts,
3623
+ useProxy: state.options.useProxy
3232
3624
  });
3233
3625
  resolve();
3234
3626
  }, { fast });
3235
3627
  });
3236
3628
  }
3237
- const usedTags = collectUsedTagNames(clone).sort();
3629
+ const usedTags = collectUsedTagNames(state.clone).sort();
3238
3630
  const tagKey = usedTags.join(",");
3239
3631
  if (cache.baseStyle.has(tagKey)) {
3240
3632
  baseCSS = cache.baseStyle.get(tagKey);
@@ -3247,69 +3639,57 @@ async function captureDOM(element, options) {
3247
3639
  }, { fast });
3248
3640
  });
3249
3641
  }
3642
+ state = { fontsCSS, baseCSS, ...state };
3643
+ await runHook("beforeRender", state);
3250
3644
  await new Promise((resolve) => {
3251
3645
  idle(() => {
3252
- const csEl = getComputedStyle(element);
3253
- function parseFilterDropShadows(cs) {
3254
- const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
3255
- if (!raw || raw === "none") {
3256
- return { bleed: { top: 0, right: 0, bottom: 0, left: 0 }, has: false };
3257
- }
3258
- const tokens = raw.match(/drop-shadow\((?:[^()]|\([^()]*\))*\)/gi) || [];
3259
- let t = 0, r = 0, b = 0, l = 0;
3260
- let found = false;
3261
- for (const tok of tokens) {
3262
- found = true;
3263
- const nums = tok.match(/-?\d+(?:\.\d+)?px/gi)?.map((v) => parseFloat(v)) || [];
3264
- const [ox = 0, oy = 0, blur = 0] = nums;
3265
- const extX = Math.abs(ox) + blur;
3266
- const extY = Math.abs(oy) + blur;
3267
- r = Math.max(r, extX + Math.max(ox, 0));
3268
- l = Math.max(l, extX + Math.max(-ox, 0));
3269
- b = Math.max(b, extY + Math.max(oy, 0));
3270
- t = Math.max(t, extY + Math.max(-oy, 0));
3271
- }
3272
- return { bleed: { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b), left: Math.ceil(l) }, has: found };
3273
- }
3274
- const rect = element.getBoundingClientRect();
3275
- const w0 = Math.max(1, Math.ceil(element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3276
- const h0 = Math.max(1, Math.ceil(element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
3646
+ const csEl = getComputedStyle(state.element);
3647
+ const rect = state.element.getBoundingClientRect();
3648
+ const w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3649
+ const h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
3277
3650
  const coerceNum = (v, def = NaN) => {
3278
3651
  const n = typeof v === "string" ? parseFloat(v) : v;
3279
3652
  return Number.isFinite(n) ? n : def;
3280
3653
  };
3281
- const optW = coerceNum(options.width);
3282
- const optH = coerceNum(options.height);
3654
+ const optW = coerceNum(state.options.width);
3655
+ const optH = coerceNum(state.options.height);
3283
3656
  let w = w0, h = h0;
3284
3657
  const hasW = Number.isFinite(optW);
3285
3658
  const hasH = Number.isFinite(optH);
3286
3659
  const aspect0 = h0 > 0 ? w0 / h0 : 1;
3287
3660
  if (hasW && hasH) {
3288
- w = Math.max(1, Math.ceil(optW));
3289
- h = Math.max(1, Math.ceil(optH));
3661
+ w = Math.max(1, limitDecimals(optW));
3662
+ h = Math.max(1, limitDecimals(optH));
3290
3663
  } else if (hasW) {
3291
- w = Math.max(1, Math.ceil(optW));
3292
- h = Math.max(1, Math.ceil(w / (aspect0 || 1)));
3664
+ w = Math.max(1, limitDecimals(optW));
3665
+ h = Math.max(1, limitDecimals(w / (aspect0 || 1)));
3293
3666
  } else if (hasH) {
3294
- h = Math.max(1, Math.ceil(optH));
3295
- w = Math.max(1, Math.ceil(h * (aspect0 || 1)));
3667
+ h = Math.max(1, limitDecimals(optH));
3668
+ w = Math.max(1, limitDecimals(h * (aspect0 || 1)));
3296
3669
  } else {
3297
3670
  w = w0;
3298
3671
  h = h0;
3299
3672
  }
3300
3673
  let minX = 0, minY = 0, maxX = w0, maxY = h0;
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 };
3674
+ if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3675
+ const M2 = {
3676
+ a: rootTransform2D.a,
3677
+ b: rootTransform2D.b || 0,
3678
+ c: rootTransform2D.c || 0,
3679
+ d: rootTransform2D.d || 1,
3680
+ e: 0,
3681
+ f: 0
3682
+ };
3303
3683
  const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
3304
- minX = bb2.minX;
3305
- minY = bb2.minY;
3306
- maxX = bb2.maxX;
3307
- maxY = bb2.maxY;
3684
+ minX = limitDecimals(bb2.minX);
3685
+ minY = limitDecimals(bb2.minY);
3686
+ maxX = limitDecimals(bb2.maxX);
3687
+ maxY = limitDecimals(bb2.maxY);
3308
3688
  } else {
3309
- const useTFBBox = !straighten && hasTFBBox(element);
3689
+ const useTFBBox = outerTransforms && hasTFBBox(state.element);
3310
3690
  if (useTFBBox) {
3311
3691
  const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3312
- const ind2 = readIndividualTransforms(element);
3692
+ const ind2 = readIndividualTransforms(state.element);
3313
3693
  const TOTAL = readTotalTransformMatrix({
3314
3694
  baseTransform: baseTransform2,
3315
3695
  rotate: ind2.rotate || "0deg",
@@ -3319,71 +3699,105 @@ async function captureDOM(element, options) {
3319
3699
  const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
3320
3700
  const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
3321
3701
  const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
3322
- minX = bb.minX;
3323
- minY = bb.minY;
3324
- maxX = bb.maxX;
3325
- maxY = bb.maxY;
3702
+ minX = limitDecimals(bb.minX);
3703
+ minY = limitDecimals(bb.minY);
3704
+ maxX = limitDecimals(bb.maxX);
3705
+ maxY = limitDecimals(bb.maxY);
3326
3706
  }
3327
3707
  }
3328
3708
  const bleedShadow = parseBoxShadow(csEl);
3329
3709
  const bleedBlur = parseFilterBlur(csEl);
3330
3710
  const bleedOutline = parseOutline(csEl);
3331
3711
  const drop = parseFilterDropShadows(csEl);
3332
- const bleed = noShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3333
- top: bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top,
3334
- right: bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right,
3335
- bottom: bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom,
3336
- left: bleedShadow.left + bleedBlur.left + bleedOutline.left + drop.bleed.left
3712
+ const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3713
+ top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
3714
+ right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
3715
+ bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
3716
+ left: limitDecimals(bleedShadow.left + bleedBlur.left + bleedOutline.left + drop.bleed.left)
3337
3717
  };
3338
- minX -= bleed.left;
3339
- minY -= bleed.top;
3340
- maxX += bleed.right;
3341
- maxY += bleed.bottom;
3342
- const vbW0 = Math.max(1, Math.ceil(maxX - minX));
3343
- const vbH0 = Math.max(1, Math.ceil(maxY - minY));
3344
- const outW = Math.max(1, Math.round(vbW0 * (hasW || hasH ? w / w0 : 1)));
3345
- const outH = Math.max(1, Math.round(vbH0 * (hasH || hasW ? h / h0 : 1)));
3718
+ minX = limitDecimals(minX - bleed.left);
3719
+ minY = limitDecimals(minY - bleed.top);
3720
+ maxX = limitDecimals(maxX + bleed.right);
3721
+ maxY = limitDecimals(maxY + bleed.bottom);
3722
+ const vbW0 = Math.max(1, limitDecimals(maxX - minX));
3723
+ const vbH0 = Math.max(1, limitDecimals(maxY - minY));
3724
+ const scaleW = hasW || hasH ? limitDecimals(w / w0) : 1;
3725
+ const scaleH = hasH || hasW ? limitDecimals(h / h0) : 1;
3726
+ const outW = Math.max(1, limitDecimals(vbW0 * scaleW));
3727
+ const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
3346
3728
  const svgNS = "http://www.w3.org/2000/svg";
3347
3729
  const basePad = isSafari() ? 1 : 0;
3348
- const extraPad = straighten ? 1 : 0;
3349
- const pad = basePad + extraPad;
3730
+ const extraPad = !outerTransforms ? 1 : 0;
3731
+ const pad = limitDecimals(basePad + extraPad);
3350
3732
  const fo = document.createElementNS(svgNS, "foreignObject");
3351
- const vbMinX = Math.floor(minX);
3352
- const vbMinY = Math.floor(minY);
3353
- fo.setAttribute("x", String(-(vbMinX - pad)));
3354
- fo.setAttribute("y", String(-(vbMinY - pad)));
3355
- fo.setAttribute("width", String(Math.ceil(w0 + pad * 2)));
3356
- fo.setAttribute("height", String(Math.ceil(h0 + pad * 2)));
3733
+ const vbMinX = limitDecimals(minX);
3734
+ const vbMinY = limitDecimals(minY);
3735
+ fo.setAttribute("x", String(limitDecimals(-(vbMinX - pad))));
3736
+ fo.setAttribute("y", String(limitDecimals(-(vbMinY - pad))));
3737
+ fo.setAttribute("width", String(limitDecimals(w0 + pad * 2)));
3738
+ fo.setAttribute("height", String(limitDecimals(h0 + pad * 2)));
3357
3739
  fo.style.overflow = "visible";
3358
3740
  const styleTag = document.createElement("style");
3359
- styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
3741
+ styleTag.textContent = state.baseCSS + state.fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + state.classCSS;
3360
3742
  fo.appendChild(styleTag);
3361
3743
  const container = document.createElement("div");
3362
3744
  container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3363
- container.style.width = `${w0}px`;
3364
- container.style.height = `${h0}px`;
3745
+ container.style.width = `${limitDecimals(w0)}px`;
3746
+ container.style.height = `${limitDecimals(h0)}px`;
3365
3747
  container.style.overflow = "visible";
3366
- clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3367
- container.appendChild(clone);
3748
+ state.clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3749
+ container.appendChild(state.clone);
3368
3750
  fo.appendChild(container);
3369
3751
  const serializer = new XMLSerializer();
3370
3752
  const foString = serializer.serializeToString(fo);
3371
- const vbW = vbW0 + pad * 2;
3372
- const vbH = vbH0 + pad * 2;
3753
+ const vbW = limitDecimals(vbW0 + pad * 2);
3754
+ const vbH = limitDecimals(vbH0 + pad * 2);
3373
3755
  const wantsSize = hasW || hasH;
3374
3756
  options.meta = { w0, h0, vbW, vbH, targetW: w, targetH: h };
3375
- const svgOutW = isSafari() && wantsSize ? vbW : outW + pad * 2;
3376
- const svgOutH = isSafari() && wantsSize ? vbH : outH + pad * 2;
3757
+ const svgOutW = isSafari() && wantsSize ? vbW : limitDecimals(outW + pad * 2);
3758
+ const svgOutH = isSafari() && wantsSize ? vbH : limitDecimals(outH + pad * 2);
3377
3759
  const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
3378
3760
  const svgFooter = "</svg>";
3379
3761
  svgString = svgHeader + foString + svgFooter;
3380
3762
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
3763
+ state = { svgString, dataURL, ...state };
3381
3764
  resolve();
3382
3765
  }, { fast });
3383
3766
  });
3767
+ await runHook("afterRender", state);
3384
3768
  const sandbox = document.getElementById("snapdom-sandbox");
3385
3769
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3386
- return dataURL;
3770
+ return state.dataURL;
3771
+ }
3772
+ var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
3773
+ function parseFilterDropShadows(cs) {
3774
+ const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
3775
+ if (!raw || raw === "none") {
3776
+ return { bleed: { top: 0, right: 0, bottom: 0, left: 0 }, has: false };
3777
+ }
3778
+ const tokens = raw.match(/drop-shadow\((?:[^()]|\([^()]*\))*\)/gi) || [];
3779
+ let t = 0, r = 0, b = 0, l = 0;
3780
+ let found = false;
3781
+ for (const tok of tokens) {
3782
+ found = true;
3783
+ const nums = tok.match(/-?\d+(?:\.\d+)?px/gi)?.map((v) => parseFloat(v)) || [];
3784
+ const [ox = 0, oy = 0, blur = 0] = nums;
3785
+ const extX = Math.abs(ox) + blur;
3786
+ const extY = Math.abs(oy) + blur;
3787
+ r = Math.max(r, extX + Math.max(ox, 0));
3788
+ l = Math.max(l, extX + Math.max(-ox, 0));
3789
+ b = Math.max(b, extY + Math.max(oy, 0));
3790
+ t = Math.max(t, extY + Math.max(-oy, 0));
3791
+ }
3792
+ return {
3793
+ bleed: {
3794
+ top: limitDecimals(t),
3795
+ right: limitDecimals(r),
3796
+ bottom: limitDecimals(b),
3797
+ left: limitDecimals(l)
3798
+ },
3799
+ has: found
3800
+ };
3387
3801
  }
3388
3802
  function normalizeRootTransforms(originalEl, cloneRoot) {
3389
3803
  if (!originalEl || !cloneRoot || !cloneRoot.style) return null;
@@ -3634,7 +4048,8 @@ function normalizeCachePolicy(v) {
3634
4048
  return "soft";
3635
4049
  }
3636
4050
  function createContext(options = {}) {
3637
- const resolvedFormat = options.format ?? "png";
4051
+ let resolvedFormat = options.format ?? "png";
4052
+ if (resolvedFormat === "jpg") resolvedFormat = "jpeg";
3638
4053
  const cachePolicy = normalizeCachePolicy(options.cache);
3639
4054
  return {
3640
4055
  // Debug & perf
@@ -3666,11 +4081,11 @@ function createContext(options = {}) {
3666
4081
  type: options.type ?? "svg",
3667
4082
  quality: options.quality ?? 0.92,
3668
4083
  dpr: options.dpr ?? (window.devicePixelRatio || 1),
3669
- backgroundColor: options.backgroundColor ?? (["jpg", "jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
4084
+ backgroundColor: options.backgroundColor ?? (["jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
3670
4085
  filename: options.filename ?? "snapDOM",
3671
4086
  // NEW flags (user-friendly)
3672
- straighten: options.straighten ?? false,
3673
- noShadows: options.noShadows ?? false
4087
+ outerTransforms: options.outerTransforms ?? true,
4088
+ outerShadows: options.outerShadows ?? false
3674
4089
  // Plugins (reservado)
3675
4090
  // plugins: normalizePlugins(...),
3676
4091
  };
@@ -3797,20 +4212,20 @@ async function toCanvas(url, options) {
3797
4212
  } else if (hasW) {
3798
4213
  const k = optW / Math.max(1, refW);
3799
4214
  outW = optW;
3800
- outH = Math.round(refH * k);
4215
+ outH = refH * k;
3801
4216
  } else if (hasH) {
3802
4217
  const k = optH / Math.max(1, refH);
3803
4218
  outH = optH;
3804
- outW = Math.round(refW * k);
4219
+ outW = refW * k;
3805
4220
  } else {
3806
4221
  outW = natW;
3807
4222
  outH = natH;
3808
4223
  }
3809
- outW = Math.round(outW * scale);
3810
- outH = Math.round(outH * scale);
4224
+ outW = outW * scale;
4225
+ outH = outH * scale;
3811
4226
  const canvas = document.createElement("canvas");
3812
- canvas.width = Math.ceil(outW * dpr);
3813
- canvas.height = Math.ceil(outH * dpr);
4227
+ canvas.width = outW * dpr;
4228
+ canvas.height = outH * dpr;
3814
4229
  canvas.style.width = `${outW}px`;
3815
4230
  canvas.style.height = `${outH}px`;
3816
4231
  const ctx = canvas.getContext("2d");
@@ -3919,16 +4334,22 @@ async function download(url, options) {
3919
4334
  }
3920
4335
 
3921
4336
  // src/api/snapdom.js
4337
+ snapdom.plugins = (...defs) => {
4338
+ registerPlugins(...defs);
4339
+ return snapdom;
4340
+ };
3922
4341
  var INTERNAL_TOKEN = Symbol("snapdom.internal");
4342
+ var INTERNAL_EXPORT_TOKEN = Symbol("snapdom.internal.silent");
3923
4343
  var _safariWarmup = false;
3924
4344
  async function snapdom(element, userOptions) {
3925
4345
  if (!element) throw new Error("Element cannot be null or undefined");
3926
4346
  const context = createContext(userOptions);
4347
+ attachSessionPlugins(context, userOptions && userOptions.plugins);
3927
4348
  if (isSafari() && (context.embedFonts === true || hasBackgroundOrMask(element))) {
3928
4349
  for (let i = 0; i < 3; i++) {
3929
4350
  try {
3930
4351
  await safariWarmup(element, userOptions);
3931
- console.log("Iteraci\xF3n n\xFAmero:", i);
4352
+ console.log("safariWarmup:", i);
3932
4353
  _safariWarmup = false;
3933
4354
  } catch {
3934
4355
  }
@@ -3943,31 +4364,90 @@ async function snapdom(element, userOptions) {
3943
4364
  }
3944
4365
  return snapdom.capture(element, context, INTERNAL_TOKEN);
3945
4366
  }
4367
+ snapdom.plugins = (...defs) => {
4368
+ registerPlugins(...defs);
4369
+ return snapdom;
4370
+ };
3946
4371
  snapdom.capture = async (el, context, _token) => {
3947
4372
  if (_token !== INTERNAL_TOKEN) throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
3948
4373
  const url = await captureDOM(el, context);
3949
- const ensureContext = (opts) => ({ ...context, ...opts || {} });
3950
- const withFormat = (format) => (opts) => {
3951
- const next = ensureContext({ ...opts || {}, format });
3952
- const wantsJpeg = format === "jpeg" || format === "jpg";
3953
- const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
3954
- if (wantsJpeg && noBg) {
3955
- next.backgroundColor = "#ffffff";
3956
- }
3957
- return rasterize(url, next);
4374
+ const coreExports = {
4375
+ img: async (ctx, opts) => toImg(url, { ...ctx, ...opts || {} }),
4376
+ svg: async (ctx, opts) => toImg(url, { ...ctx, ...opts || {} }),
4377
+ canvas: async (ctx, opts) => toCanvas(url, { ...ctx, ...opts || {} }),
4378
+ blob: async (ctx, opts) => toBlob(url, { ...ctx, ...opts || {} }),
4379
+ png: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "png" }),
4380
+ jpeg: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "jpeg" }),
4381
+ webp: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "webp" }),
4382
+ download: async (ctx, opts) => download(url, { ...ctx, ...opts || {} })
3958
4383
  };
3959
- return {
4384
+ const _pluginExports = {
4385
+ svg: (opts) => toImg(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4386
+ canvas: (opts) => toCanvas(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4387
+ png: (opts) => rasterize(url, { ...context, ...opts || {}, format: "png", [INTERNAL_EXPORT_TOKEN]: true }),
4388
+ jpeg: (opts) => rasterize(url, { ...context, ...opts || {}, format: "jpeg", [INTERNAL_EXPORT_TOKEN]: true }),
4389
+ jpg: (opts) => rasterize(url, { ...context, ...opts || {}, format: "jpeg", [INTERNAL_EXPORT_TOKEN]: true }),
4390
+ webp: (opts) => rasterize(url, { ...context, ...opts || {}, format: "webp", [INTERNAL_EXPORT_TOKEN]: true }),
4391
+ blob: (opts) => toBlob(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4392
+ img: (opts) => toImg(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true })
4393
+ };
4394
+ const _defineCtx = { ...context, export: { url }, exports: _pluginExports };
4395
+ const providedMaps = await runAll("defineExports", _defineCtx);
4396
+ const provided = Object.assign({}, ...providedMaps.filter((x) => x && typeof x === "object"));
4397
+ const exportsMap = { ...coreExports, ...provided };
4398
+ if (exportsMap.jpeg && !exportsMap.jpg) {
4399
+ exportsMap.jpg = (ctx, opts) => exportsMap.jpeg(ctx, opts);
4400
+ }
4401
+ function normalizeExportOptions(type, opts) {
4402
+ const next = { ...context, ...opts || {} };
4403
+ if (type === "jpeg" || type === "jpg") {
4404
+ const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
4405
+ if (noBg) next.backgroundColor = "#ffffff";
4406
+ }
4407
+ return next;
4408
+ }
4409
+ let afterSnapFired = false;
4410
+ let _exportQueue = Promise.resolve();
4411
+ async function runExport(type, opts) {
4412
+ const job = async () => {
4413
+ const work = exportsMap[type];
4414
+ if (!work) throw new Error(`[snapdom] Unknown export type: ${type}`);
4415
+ const nextOpts = normalizeExportOptions(type, opts);
4416
+ const ctx = { ...context, export: { type, options: nextOpts, url } };
4417
+ await runHook("beforeExport", ctx);
4418
+ const result2 = await work(ctx, nextOpts);
4419
+ await runHook("afterExport", ctx, result2);
4420
+ if (!afterSnapFired) {
4421
+ afterSnapFired = true;
4422
+ await runHook("afterSnap", context);
4423
+ }
4424
+ return result2;
4425
+ };
4426
+ return _exportQueue = _exportQueue.then(job);
4427
+ }
4428
+ const result = {
3960
4429
  url,
3961
4430
  toRaw: () => url,
3962
- toImg: (opts) => toImg(url, ensureContext(opts)),
3963
- toSvg: (opts) => toImg(url, ensureContext(opts)),
3964
- toCanvas: (opts) => toCanvas(url, ensureContext(opts)),
3965
- toBlob: (opts) => toBlob(url, ensureContext(opts)),
3966
- toPng: withFormat("png"),
3967
- toJpg: withFormat("jpeg"),
3968
- toWebp: withFormat("webp"),
3969
- download: (opts) => download(url, ensureContext(opts))
4431
+ to: (type, opts) => runExport(type, opts),
4432
+ // Métodos “clásicos” que los tests esperan:
4433
+ toImg: (opts) => runExport("img", opts),
4434
+ toSvg: (opts) => runExport("svg", opts),
4435
+ toCanvas: (opts) => runExport("canvas", opts),
4436
+ toBlob: (opts) => runExport("blob", opts),
4437
+ toPng: (opts) => runExport("png", opts),
4438
+ toJpg: (opts) => runExport("jpg", opts),
4439
+ // ← alias requerido por los tests
4440
+ toWebp: (opts) => runExport("webp", opts),
4441
+ download: (opts) => runExport("download", opts)
4442
+ // ← método directo (no toDownload)
3970
4443
  };
4444
+ for (const key of Object.keys(exportsMap)) {
4445
+ const helper = "to" + key.charAt(0).toUpperCase() + key.slice(1);
4446
+ if (!result[helper]) {
4447
+ result[helper] = (opts) => runExport(key, opts);
4448
+ }
4449
+ }
4450
+ return result;
3971
4451
  };
3972
4452
  snapdom.toRaw = (el, options) => snapdom(el, options).then((result) => result.toRaw());
3973
4453
  snapdom.toImg = (el, options) => snapdom(el, options).then((result) => result.toImg());
@@ -3990,30 +4470,47 @@ async function safariWarmup(element, baseOptions) {
3990
4470
  try {
3991
4471
  url = await captureDOM(element, preflight);
3992
4472
  } catch {
3993
- return;
3994
4473
  }
3995
- await new Promise((resolve) => {
3996
- const img = new Image();
3997
- img.decoding = "sync";
3998
- img.loading = "eager";
3999
- img.style.position = "fixed";
4000
- img.style.left = 0;
4001
- img.style.top = 0;
4002
- img.style.width = "10px";
4003
- img.style.height = "10px";
4004
- img.style.opacity = "0.01";
4005
- img.style.transform = "translateZ(10px)";
4006
- img.style.willChange = "transform,opacity;";
4007
- img.src = url;
4008
- const cleanup = async () => {
4009
- await new Promise((r) => setTimeout(r, 100));
4010
- if (img && img.parentNode) img.parentNode.removeChild(img);
4011
- _safariWarmup = true;
4012
- resolve();
4013
- };
4014
- document.body.appendChild(img);
4015
- cleanup();
4474
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
4475
+ if (url) {
4476
+ await new Promise((resolve) => {
4477
+ const img = new Image();
4478
+ try {
4479
+ img.decoding = "sync";
4480
+ img.loading = "eager";
4481
+ } catch {
4482
+ }
4483
+ img.style.cssText = "position:fixed;left:0px;top:0px;width:10px;height:10px;opacity:0.01;pointer-events:none;";
4484
+ img.src = url;
4485
+ document.body.appendChild(img);
4486
+ (async () => {
4487
+ try {
4488
+ if (typeof img.decode === "function") await img.decode();
4489
+ } catch {
4490
+ }
4491
+ const start = performance.now();
4492
+ while (!(img.complete && img.naturalWidth > 0) && performance.now() - start < 900) {
4493
+ await new Promise((r) => setTimeout(r, 50));
4494
+ }
4495
+ await new Promise((r) => requestAnimationFrame(r));
4496
+ try {
4497
+ img.remove();
4498
+ } catch {
4499
+ }
4500
+ resolve();
4501
+ })();
4502
+ });
4503
+ }
4504
+ element.querySelectorAll("canvas").forEach((c) => {
4505
+ try {
4506
+ const ctx = c.getContext("2d", { willReadFrequently: true });
4507
+ if (ctx) {
4508
+ ctx.getImageData(0, 0, 1, 1);
4509
+ }
4510
+ } catch {
4511
+ }
4016
4512
  });
4513
+ _safariWarmup = true;
4017
4514
  }
4018
4515
  function hasBackgroundOrMask(el) {
4019
4516
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_ELEMENT);
@@ -4026,6 +4523,7 @@ function hasBackgroundOrMask(el) {
4026
4523
  const bg = cs.backgroundImage && cs.backgroundImage !== "none";
4027
4524
  const mask = cs.maskImage && cs.maskImage !== "none" || cs.webkitMaskImage && cs.webkitMaskImage !== "none";
4028
4525
  if (bg || mask) return true;
4526
+ if (node.tagName === "CANVAS") return true;
4029
4527
  }
4030
4528
  return false;
4031
4529
  }
@@ -4051,6 +4549,7 @@ async function preCache(root = document, options = {}) {
4051
4549
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
4052
4550
  }
4053
4551
  cache.image = cache.image || /* @__PURE__ */ new Map();
4552
+ cache.background = cache.background || /* @__PURE__ */ new Map();
4054
4553
  try {
4055
4554
  await inlineBackgroundImages(
4056
4555
  root,
@@ -4063,7 +4562,13 @@ async function preCache(root = document, options = {}) {
4063
4562
  }
4064
4563
  let imgEls = [], allEls = [];
4065
4564
  try {
4066
- if (root?.querySelectorAll) {
4565
+ if (root && root.nodeType === 1) {
4566
+ const descendants = root.querySelectorAll ? Array.from(root.querySelectorAll("*")) : [];
4567
+ allEls = [root, ...descendants];
4568
+ imgEls = [];
4569
+ if (root.tagName === "IMG" && root.getAttribute("src")) imgEls.push(root);
4570
+ imgEls.push(...Array.from(root.querySelectorAll?.("img[src]") || []));
4571
+ } else if (root?.querySelectorAll) {
4067
4572
  imgEls = Array.from(root.querySelectorAll("img[src]"));
4068
4573
  allEls = Array.from(root.querySelectorAll("*"));
4069
4574
  }
@@ -4087,17 +4592,18 @@ async function preCache(root = document, options = {}) {
4087
4592
  for (const el of allEls) {
4088
4593
  let bg = "";
4089
4594
  try {
4090
- bg = getStyle(el).backgroundImage;
4595
+ bg = el?.style?.backgroundImage || "";
4596
+ if (!bg || bg === "none") {
4597
+ bg = getStyle(el).backgroundImage;
4598
+ }
4091
4599
  } catch {
4092
4600
  }
4093
4601
  if (bg && bg !== "none") {
4094
- const parts = splitBackgroundImage(bg);
4095
- for (const entry of parts) {
4096
- if (entry.startsWith("url(")) {
4097
- const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
4098
- });
4099
- promises.push(p);
4100
- }
4602
+ const urlEntries = bg.match(/url\((?:[^()"']+|"(?:[^"]*)"|'(?:[^']*)')\)/gi) || [];
4603
+ for (const entry of urlEntries) {
4604
+ const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
4605
+ });
4606
+ promises.push(p);
4101
4607
  }
4102
4608
  }
4103
4609
  }