@zumer/snapdom 1.9.14 → 2.0.0-dev.0

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.0
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;
@@ -2176,6 +2257,8 @@ function formatCounter(value, style) {
2176
2257
  }
2177
2258
  }
2178
2259
  function buildCounterContext(root) {
2260
+ const getEpoch = () => cache?.session?.__counterEpoch ?? 0;
2261
+ let run = getEpoch();
2179
2262
  const nodeCounters = /* @__PURE__ */ new WeakMap();
2180
2263
  const rootEl = root instanceof Document ? root.documentElement : root;
2181
2264
  const isLi = (el) => el && el.tagName === "LI";
@@ -2268,6 +2351,14 @@ function buildCounterContext(root) {
2268
2351
  };
2269
2352
  const empty = /* @__PURE__ */ new Map();
2270
2353
  build(rootEl, empty, empty);
2354
+ function ensureFresh() {
2355
+ const now = getEpoch();
2356
+ if (now !== run) {
2357
+ run = now;
2358
+ const empty2 = /* @__PURE__ */ new Map();
2359
+ build(rootEl, empty2, empty2);
2360
+ }
2361
+ }
2271
2362
  return {
2272
2363
  /**
2273
2364
  * Get top value for counter name at given node.
@@ -2275,6 +2366,7 @@ function buildCounterContext(root) {
2275
2366
  * @param {string} name
2276
2367
  */
2277
2368
  get(node, name) {
2369
+ ensureFresh();
2278
2370
  const s = nodeCounters.get(node)?.get(name);
2279
2371
  return s && s.length ? s[s.length - 1] : 0;
2280
2372
  },
@@ -2284,6 +2376,7 @@ function buildCounterContext(root) {
2284
2376
  * @param {string} name
2285
2377
  */
2286
2378
  getStack(node, name) {
2379
+ ensureFresh();
2287
2380
  const s = nodeCounters.get(node)?.get(name);
2288
2381
  return s ? s.slice() : [];
2289
2382
  }
@@ -2317,8 +2410,8 @@ function resolveCountersInContent(raw, node, ctx) {
2317
2410
  }
2318
2411
 
2319
2412
  // src/modules/pseudo.js
2320
- var counterCtx = null;
2321
2413
  var __siblingCounters = /* @__PURE__ */ new WeakMap();
2414
+ var __pseudoEpoch = -1;
2322
2415
  function unquoteDoubleStrings2(s) {
2323
2416
  return (s || "").replace(/"([^"]*)"/g, "$1");
2324
2417
  }
@@ -2415,12 +2508,19 @@ function resolvePseudoContentAndIncs(node, pseudo, baseCtx) {
2415
2508
  }
2416
2509
  async function inlinePseudoElements(source, clone, sessionCache, options) {
2417
2510
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
2418
- if (!counterCtx) {
2511
+ const epoch = cache?.session?.__counterEpoch ?? 0;
2512
+ if (__pseudoEpoch !== epoch) {
2513
+ __siblingCounters = /* @__PURE__ */ new WeakMap();
2514
+ if (sessionCache) sessionCache.__counterCtx = null;
2515
+ __pseudoEpoch = epoch;
2516
+ }
2517
+ if (!sessionCache.__counterCtx) {
2419
2518
  try {
2420
- counterCtx = buildCounterContext(source.ownerDocument || document);
2519
+ sessionCache.__counterCtx = buildCounterContext(source.ownerDocument || document);
2421
2520
  } catch {
2422
2521
  }
2423
2522
  }
2523
+ const counterCtx = sessionCache.__counterCtx;
2424
2524
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
2425
2525
  try {
2426
2526
  const style = getStyle(source, pseudo);
@@ -2485,7 +2585,8 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2485
2585
  }
2486
2586
  const pseudoEl = document.createElement("span");
2487
2587
  pseudoEl.dataset.snapdomPseudo = pseudo;
2488
- pseudoEl.style.verticalAlign = "middle";
2588
+ pseudoEl.style.display = "inline";
2589
+ pseudoEl.style.verticalAlign = "baseline";
2489
2590
  pseudoEl.style.pointerEvents = "none";
2490
2591
  const snapshot = snapshotComputedStyle(style);
2491
2592
  const key = getStyleKey(snapshot, "span");
@@ -2513,9 +2614,23 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2513
2614
  } else if (!isIconFont22 && hasExplicitContent) {
2514
2615
  pseudoEl.textContent = cleanContent;
2515
2616
  }
2516
- pseudoEl.style.background = "none";
2517
- if ("mask" in pseudoEl.style) {
2518
- pseudoEl.style.mask = "none";
2617
+ pseudoEl.style.backgroundImage = "none";
2618
+ if ("maskImage" in pseudoEl.style) pseudoEl.style.maskImage = "none";
2619
+ if ("webkitMaskImage" in pseudoEl.style) pseudoEl.style.webkitMaskImage = "none";
2620
+ try {
2621
+ pseudoEl.style.backgroundRepeat = style.backgroundRepeat;
2622
+ pseudoEl.style.backgroundSize = style.backgroundSize;
2623
+ if (style.backgroundPositionX && style.backgroundPositionY) {
2624
+ pseudoEl.style.backgroundPositionX = style.backgroundPositionX;
2625
+ pseudoEl.style.backgroundPositionY = style.backgroundPositionY;
2626
+ } else {
2627
+ pseudoEl.style.backgroundPosition = style.backgroundPosition;
2628
+ }
2629
+ pseudoEl.style.backgroundOrigin = style.backgroundOrigin;
2630
+ pseudoEl.style.backgroundClip = style.backgroundClip;
2631
+ pseudoEl.style.backgroundAttachment = style.backgroundAttachment;
2632
+ pseudoEl.style.backgroundBlendMode = style.backgroundBlendMode;
2633
+ } catch {
2519
2634
  }
2520
2635
  if (hasBg) {
2521
2636
  try {
@@ -2558,53 +2673,113 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
2558
2673
  }
2559
2674
 
2560
2675
  // 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));
2676
+ function inlineExternalDefsAndSymbols(element, lookupRoot) {
2677
+ if (!element || !(element instanceof Element)) return;
2678
+ const doc = element.ownerDocument || document;
2679
+ const searchRoot = lookupRoot || doc;
2680
+ const svgRoots = element instanceof SVGSVGElement ? [element] : Array.from(element.querySelectorAll("svg"));
2681
+ if (svgRoots.length === 0) return;
2682
+ const URL_ID_RE = /url\(\s*#([^)]+)\)/g;
2683
+ const URL_ATTRS = [
2684
+ "fill",
2685
+ "stroke",
2686
+ "filter",
2687
+ "clip-path",
2688
+ "mask",
2689
+ "marker",
2690
+ "marker-start",
2691
+ "marker-mid",
2692
+ "marker-end"
2693
+ ];
2694
+ const cssEscape = (s) => window.CSS && CSS.escape ? CSS.escape(s) : s.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
2695
+ const globalExistingIds = new Set(
2696
+ Array.from(element.querySelectorAll("[id]")).map((n) => n.id)
2697
+ );
2698
+ const neededIds = /* @__PURE__ */ new Set();
2699
+ let sawAnyReference = false;
2700
+ const addUrlIdsFromValue = (val) => {
2701
+ if (!val) return;
2702
+ URL_ID_RE.lastIndex = 0;
2703
+ let m;
2704
+ while (m = URL_ID_RE.exec(val)) {
2705
+ sawAnyReference = true;
2706
+ const id = (m[1] || "").trim();
2707
+ if (id && !globalExistingIds.has(id)) neededIds.add(id);
2568
2708
  }
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;
2709
+ };
2710
+ const collectReferencesInSvg = (rootSvg) => {
2711
+ const uses = rootSvg.querySelectorAll('use[href^="#"], use[xlink\\:href^="#"]');
2712
+ for (const u of uses) {
2713
+ const href = u.getAttribute("href") || u.getAttribute("xlink:href");
2714
+ if (!href || !href.startsWith("#")) continue;
2715
+ sawAnyReference = true;
2716
+ const id = href.slice(1).trim();
2717
+ if (id && !globalExistingIds.has(id)) neededIds.add(id);
2593
2718
  }
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;
2719
+ 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("]';
2720
+ const candidates = rootSvg.querySelectorAll(query);
2721
+ for (const el of candidates) {
2722
+ addUrlIdsFromValue(el.getAttribute("style") || "");
2723
+ for (const a of URL_ATTRS) addUrlIdsFromValue(el.getAttribute(a));
2724
+ }
2725
+ };
2726
+ for (const svg of svgRoots) collectReferencesInSvg(svg);
2727
+ if (!sawAnyReference) return;
2728
+ let defsHost = element.querySelector("svg.inline-defs-container");
2729
+ if (!defsHost) {
2730
+ defsHost = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
2731
+ defsHost.classList.add("inline-defs-container");
2732
+ defsHost.setAttribute("aria-hidden", "true");
2733
+ defsHost.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden");
2734
+ element.insertBefore(defsHost, element.firstChild || null);
2735
+ }
2736
+ let localDefs = defsHost.querySelector("defs") || null;
2737
+ const findGlobalById = (id) => {
2738
+ if (!id) return null;
2739
+ if (globalExistingIds.has(id)) return null;
2740
+ const esc = cssEscape(id);
2741
+ const tryFind = (sel) => {
2742
+ const el = searchRoot.querySelector(sel);
2743
+ return el && !element.contains(el) ? el : null;
2744
+ };
2745
+ return tryFind(`svg defs > *#${esc}`) || tryFind(`svg > symbol#${esc}`) || tryFind(`*#${esc}`);
2746
+ };
2747
+ if (!neededIds.size) return;
2748
+ const queued = new Set(neededIds);
2749
+ const inlined = /* @__PURE__ */ new Set();
2750
+ while (queued.size) {
2751
+ const id = queued.values().next().value;
2752
+ queued.delete(id);
2753
+ if (!id || globalExistingIds.has(id) || inlined.has(id)) continue;
2754
+ const source = findGlobalById(id);
2755
+ if (!source) {
2756
+ inlined.add(id);
2757
+ continue;
2758
+ }
2759
+ if (!localDefs) {
2760
+ localDefs = doc.createElementNS("http://www.w3.org/2000/svg", "defs");
2761
+ defsHost.appendChild(localDefs);
2762
+ }
2763
+ const clone = source.cloneNode(true);
2764
+ if (!clone.id) clone.setAttribute("id", id);
2765
+ localDefs.appendChild(clone);
2766
+ inlined.add(id);
2767
+ globalExistingIds.add(id);
2768
+ const walk = [clone, ...clone.querySelectorAll("*")];
2769
+ for (const node of walk) {
2770
+ const h = node.getAttribute?.("href") || node.getAttribute?.("xlink:href");
2771
+ if (h && h.startsWith("#")) {
2772
+ const ref = h.slice(1).trim();
2773
+ if (ref && !globalExistingIds.has(ref) && !inlined.has(ref)) queued.add(ref);
2774
+ }
2775
+ const style = node.getAttribute?.("style") || "";
2776
+ if (style) addUrlIdsFromValue(style);
2777
+ for (const a of URL_ATTRS) {
2778
+ const v = node.getAttribute?.(a);
2779
+ if (v) addUrlIdsFromValue(v);
2605
2780
  }
2606
2781
  }
2607
- });
2782
+ }
2608
2783
  }
2609
2784
 
2610
2785
  // src/modules/changeCSS.js
@@ -3050,6 +3225,17 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3050
3225
  "-webkit-mask-position-x",
3051
3226
  "-webkit-mask-position-y"
3052
3227
  ];
3228
+ const BG_LAYOUT_PROPS = [
3229
+ "background-position",
3230
+ "background-position-x",
3231
+ "background-position-y",
3232
+ "background-size",
3233
+ "background-repeat",
3234
+ "background-origin",
3235
+ "background-clip",
3236
+ "background-attachment",
3237
+ "background-blend-mode"
3238
+ ];
3053
3239
  const BORDER_AUX_PROPS = [
3054
3240
  "border-image-slice",
3055
3241
  "border-image-width",
@@ -3065,6 +3251,11 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3065
3251
  const bis = style.getPropertyValue("border-image-source");
3066
3252
  return bi && bi !== "none" || bis && bis !== "none";
3067
3253
  })();
3254
+ for (const prop of BG_LAYOUT_PROPS) {
3255
+ const v = style.getPropertyValue(prop);
3256
+ if (!v) continue;
3257
+ cloneNode.style.setProperty(prop, v);
3258
+ }
3068
3259
  for (const prop of URL_PROPS) {
3069
3260
  const val = style.getPropertyValue(prop);
3070
3261
  if (!val || val === "none") continue;
@@ -3089,7 +3280,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
3089
3280
  }
3090
3281
  }
3091
3282
  const sChildren = Array.from(srcNode.children);
3092
- const cChildren = Array.from(cloneNode.children);
3283
+ const cChildren = Array.from(cloneNode.children).filter((el) => !(el.dataset && el.dataset.snapdomPseudo));
3093
3284
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
3094
3285
  queue.push([sChildren[i], cChildren[i]]);
3095
3286
  }
@@ -3153,6 +3344,81 @@ function isPlainTextContainer(el) {
3153
3344
  return Array.from(el.childNodes).some((n) => n.nodeType === Node.TEXT_NODE);
3154
3345
  }
3155
3346
 
3347
+ // src/core/plugins.js
3348
+ var __plugins = [];
3349
+ function normalizePlugin(spec) {
3350
+ if (!spec) return null;
3351
+ if (Array.isArray(spec)) {
3352
+ const [factory, options] = spec;
3353
+ return typeof factory === "function" ? factory(options) : factory;
3354
+ }
3355
+ if (typeof spec === "object" && "plugin" in spec) {
3356
+ const { plugin, options } = spec;
3357
+ return typeof plugin === "function" ? plugin(options) : plugin;
3358
+ }
3359
+ if (typeof spec === "function") return spec();
3360
+ return spec;
3361
+ }
3362
+ function registerPlugins(...defs) {
3363
+ const flat = defs.flat();
3364
+ for (const d of flat) {
3365
+ const inst = normalizePlugin(d);
3366
+ if (!inst) continue;
3367
+ if (!__plugins.some((p) => p && p.name && inst.name && p.name === inst.name)) {
3368
+ __plugins.push(inst);
3369
+ }
3370
+ }
3371
+ }
3372
+ function getContextPlugins(context) {
3373
+ const arr = context && Array.isArray(context.plugins) ? context.plugins : __plugins;
3374
+ return arr || __plugins;
3375
+ }
3376
+ async function runHook(name, context, payload) {
3377
+ let acc = payload;
3378
+ const list = getContextPlugins(context);
3379
+ for (const p of list) {
3380
+ const fn = p && typeof p[name] === "function" ? p[name] : null;
3381
+ if (!fn) continue;
3382
+ const out = await fn(context, acc);
3383
+ if (typeof out !== "undefined") acc = out;
3384
+ }
3385
+ return acc;
3386
+ }
3387
+ async function runAll(name, context, payload) {
3388
+ const outs = [];
3389
+ const list = getContextPlugins(context);
3390
+ for (const p of list) {
3391
+ const fn = p && typeof p[name] === "function" ? p[name] : null;
3392
+ if (!fn) continue;
3393
+ const out = await fn(context, payload);
3394
+ if (typeof out !== "undefined") outs.push(out);
3395
+ }
3396
+ return outs;
3397
+ }
3398
+ function mergePlugins(localDefs) {
3399
+ const out = [];
3400
+ if (Array.isArray(localDefs)) {
3401
+ for (const d of localDefs) {
3402
+ const inst = normalizePlugin(d);
3403
+ if (!inst || !inst.name) continue;
3404
+ const i = out.findIndex((x) => x && x.name === inst.name);
3405
+ if (i >= 0) out.splice(i, 1);
3406
+ out.push(inst);
3407
+ }
3408
+ }
3409
+ for (const g of __plugins) {
3410
+ if (g && g.name && !out.some((x) => x.name === g.name)) {
3411
+ out.push(g);
3412
+ }
3413
+ }
3414
+ return Object.freeze(out);
3415
+ }
3416
+ function attachSessionPlugins(context, localDefs, force = false) {
3417
+ if (!context || context.plugins && !force) return context;
3418
+ context.plugins = mergePlugins(localDefs);
3419
+ return context;
3420
+ }
3421
+
3156
3422
  // src/core/capture.js
3157
3423
  function stripRootShadows(originalEl, cloneRoot) {
3158
3424
  if (!originalEl || !cloneRoot || !cloneRoot.style) return;
@@ -3182,41 +3448,46 @@ async function captureDOM(element, options) {
3182
3448
  const fast = options.fast;
3183
3449
  const straighten = !!options.straighten;
3184
3450
  const noShadows = !!options.noShadows;
3451
+ let state = { element, options, plugins: options.plugins };
3185
3452
  let clone, classCSS, styleCache;
3186
3453
  let fontsCSS = "";
3187
3454
  let baseCSS = "";
3188
3455
  let dataURL;
3189
3456
  let svgString;
3190
3457
  let rootTransform2D = null;
3191
- const undoClamp = lineClamp(element);
3458
+ await runHook("beforeSnap", state);
3459
+ await runHook("beforeClone", state);
3460
+ const undoClamp = lineClamp(state.element);
3192
3461
  try {
3193
- ({ clone, classCSS, styleCache } = await prepareClone(element, options));
3462
+ ({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
3194
3463
  if (straighten && clone) {
3195
- rootTransform2D = normalizeRootTransforms(element, clone);
3464
+ rootTransform2D = normalizeRootTransforms(state.element, state.clone);
3196
3465
  }
3197
3466
  if (noShadows && clone) {
3198
- stripRootShadows(element, clone);
3467
+ stripRootShadows(state.element, state.clone);
3199
3468
  }
3200
3469
  } finally {
3201
3470
  undoClamp();
3202
3471
  }
3472
+ state = { clone, classCSS, styleCache, ...state };
3473
+ await runHook("afterClone", state);
3203
3474
  await new Promise((resolve) => {
3204
3475
  idle(async () => {
3205
- await inlineImages(clone, options);
3476
+ await inlineImages(state.clone, state.options);
3206
3477
  resolve();
3207
3478
  }, { fast });
3208
3479
  });
3209
3480
  await new Promise((resolve) => {
3210
3481
  idle(async () => {
3211
- await inlineBackgroundImages(element, clone, styleCache, options);
3482
+ await inlineBackgroundImages(state.element, state.clone, state.styleCache, state.options);
3212
3483
  resolve();
3213
3484
  }, { fast });
3214
3485
  });
3215
3486
  if (options.embedFonts) {
3216
3487
  await new Promise((resolve) => {
3217
3488
  idle(async () => {
3218
- const required = collectUsedFontVariants(element);
3219
- const usedCodepoints = collectUsedCodepoints(element);
3489
+ const required = collectUsedFontVariants(state.element);
3490
+ const usedCodepoints = collectUsedCodepoints(state.element);
3220
3491
  if (isSafari()) {
3221
3492
  const families = new Set(
3222
3493
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
@@ -3227,14 +3498,14 @@ async function captureDOM(element, options) {
3227
3498
  required,
3228
3499
  usedCodepoints,
3229
3500
  preCached: false,
3230
- exclude: options.excludeFonts,
3231
- useProxy: options.useProxy
3501
+ exclude: state.options.excludeFonts,
3502
+ useProxy: state.options.useProxy
3232
3503
  });
3233
3504
  resolve();
3234
3505
  }, { fast });
3235
3506
  });
3236
3507
  }
3237
- const usedTags = collectUsedTagNames(clone).sort();
3508
+ const usedTags = collectUsedTagNames(state.clone).sort();
3238
3509
  const tagKey = usedTags.join(",");
3239
3510
  if (cache.baseStyle.has(tagKey)) {
3240
3511
  baseCSS = cache.baseStyle.get(tagKey);
@@ -3247,69 +3518,57 @@ async function captureDOM(element, options) {
3247
3518
  }, { fast });
3248
3519
  });
3249
3520
  }
3521
+ state = { fontsCSS, baseCSS, ...state };
3522
+ await runHook("beforeRender", state);
3250
3523
  await new Promise((resolve) => {
3251
3524
  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));
3525
+ const csEl = getComputedStyle(state.element);
3526
+ const rect = state.element.getBoundingClientRect();
3527
+ const w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
3528
+ const h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
3277
3529
  const coerceNum = (v, def = NaN) => {
3278
3530
  const n = typeof v === "string" ? parseFloat(v) : v;
3279
3531
  return Number.isFinite(n) ? n : def;
3280
3532
  };
3281
- const optW = coerceNum(options.width);
3282
- const optH = coerceNum(options.height);
3533
+ const optW = coerceNum(state.options.width);
3534
+ const optH = coerceNum(state.options.height);
3283
3535
  let w = w0, h = h0;
3284
3536
  const hasW = Number.isFinite(optW);
3285
3537
  const hasH = Number.isFinite(optH);
3286
3538
  const aspect0 = h0 > 0 ? w0 / h0 : 1;
3287
3539
  if (hasW && hasH) {
3288
- w = Math.max(1, Math.ceil(optW));
3289
- h = Math.max(1, Math.ceil(optH));
3540
+ w = Math.max(1, limitDecimals(optW));
3541
+ h = Math.max(1, limitDecimals(optH));
3290
3542
  } else if (hasW) {
3291
- w = Math.max(1, Math.ceil(optW));
3292
- h = Math.max(1, Math.ceil(w / (aspect0 || 1)));
3543
+ w = Math.max(1, limitDecimals(optW));
3544
+ h = Math.max(1, limitDecimals(w / (aspect0 || 1)));
3293
3545
  } else if (hasH) {
3294
- h = Math.max(1, Math.ceil(optH));
3295
- w = Math.max(1, Math.ceil(h * (aspect0 || 1)));
3546
+ h = Math.max(1, limitDecimals(optH));
3547
+ w = Math.max(1, limitDecimals(h * (aspect0 || 1)));
3296
3548
  } else {
3297
3549
  w = w0;
3298
3550
  h = h0;
3299
3551
  }
3300
3552
  let minX = 0, minY = 0, maxX = w0, maxY = h0;
3301
3553
  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 };
3554
+ const M2 = {
3555
+ a: rootTransform2D.a,
3556
+ b: rootTransform2D.b || 0,
3557
+ c: rootTransform2D.c || 0,
3558
+ d: rootTransform2D.d || 1,
3559
+ e: 0,
3560
+ f: 0
3561
+ };
3303
3562
  const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
3304
- minX = bb2.minX;
3305
- minY = bb2.minY;
3306
- maxX = bb2.maxX;
3307
- maxY = bb2.maxY;
3563
+ minX = limitDecimals(bb2.minX);
3564
+ minY = limitDecimals(bb2.minY);
3565
+ maxX = limitDecimals(bb2.maxX);
3566
+ maxY = limitDecimals(bb2.maxY);
3308
3567
  } else {
3309
- const useTFBBox = !straighten && hasTFBBox(element);
3568
+ const useTFBBox = !straighten && hasTFBBox(state.element);
3310
3569
  if (useTFBBox) {
3311
3570
  const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3312
- const ind2 = readIndividualTransforms(element);
3571
+ const ind2 = readIndividualTransforms(state.element);
3313
3572
  const TOTAL = readTotalTransformMatrix({
3314
3573
  baseTransform: baseTransform2,
3315
3574
  rotate: ind2.rotate || "0deg",
@@ -3319,10 +3578,10 @@ async function captureDOM(element, options) {
3319
3578
  const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
3320
3579
  const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
3321
3580
  const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
3322
- minX = bb.minX;
3323
- minY = bb.minY;
3324
- maxX = bb.maxX;
3325
- maxY = bb.maxY;
3581
+ minX = limitDecimals(bb.minX);
3582
+ minY = limitDecimals(bb.minY);
3583
+ maxX = limitDecimals(bb.maxX);
3584
+ maxY = limitDecimals(bb.maxY);
3326
3585
  }
3327
3586
  }
3328
3587
  const bleedShadow = parseBoxShadow(csEl);
@@ -3330,60 +3589,94 @@ async function captureDOM(element, options) {
3330
3589
  const bleedOutline = parseOutline(csEl);
3331
3590
  const drop = parseFilterDropShadows(csEl);
3332
3591
  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
3592
+ top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
3593
+ right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
3594
+ bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
3595
+ left: limitDecimals(bleedShadow.left + bleedBlur.left + bleedOutline.left + drop.bleed.left)
3337
3596
  };
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)));
3597
+ minX = limitDecimals(minX - bleed.left);
3598
+ minY = limitDecimals(minY - bleed.top);
3599
+ maxX = limitDecimals(maxX + bleed.right);
3600
+ maxY = limitDecimals(maxY + bleed.bottom);
3601
+ const vbW0 = Math.max(1, limitDecimals(maxX - minX));
3602
+ const vbH0 = Math.max(1, limitDecimals(maxY - minY));
3603
+ const scaleW = hasW || hasH ? limitDecimals(w / w0) : 1;
3604
+ const scaleH = hasH || hasW ? limitDecimals(h / h0) : 1;
3605
+ const outW = Math.max(1, limitDecimals(vbW0 * scaleW));
3606
+ const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
3346
3607
  const svgNS = "http://www.w3.org/2000/svg";
3347
3608
  const basePad = isSafari() ? 1 : 0;
3348
3609
  const extraPad = straighten ? 1 : 0;
3349
- const pad = basePad + extraPad;
3610
+ const pad = limitDecimals(basePad + extraPad);
3350
3611
  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)));
3612
+ const vbMinX = limitDecimals(minX);
3613
+ const vbMinY = limitDecimals(minY);
3614
+ fo.setAttribute("x", String(limitDecimals(-(vbMinX - pad))));
3615
+ fo.setAttribute("y", String(limitDecimals(-(vbMinY - pad))));
3616
+ fo.setAttribute("width", String(limitDecimals(w0 + pad * 2)));
3617
+ fo.setAttribute("height", String(limitDecimals(h0 + pad * 2)));
3357
3618
  fo.style.overflow = "visible";
3358
3619
  const styleTag = document.createElement("style");
3359
- styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
3620
+ styleTag.textContent = state.baseCSS + state.fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + state.classCSS;
3360
3621
  fo.appendChild(styleTag);
3361
3622
  const container = document.createElement("div");
3362
3623
  container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3363
- container.style.width = `${w0}px`;
3364
- container.style.height = `${h0}px`;
3624
+ container.style.width = `${limitDecimals(w0)}px`;
3625
+ container.style.height = `${limitDecimals(h0)}px`;
3365
3626
  container.style.overflow = "visible";
3366
- clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3367
- container.appendChild(clone);
3627
+ state.clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3628
+ container.appendChild(state.clone);
3368
3629
  fo.appendChild(container);
3369
3630
  const serializer = new XMLSerializer();
3370
3631
  const foString = serializer.serializeToString(fo);
3371
- const vbW = vbW0 + pad * 2;
3372
- const vbH = vbH0 + pad * 2;
3632
+ const vbW = limitDecimals(vbW0 + pad * 2);
3633
+ const vbH = limitDecimals(vbH0 + pad * 2);
3373
3634
  const wantsSize = hasW || hasH;
3374
3635
  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;
3636
+ const svgOutW = isSafari() && wantsSize ? vbW : limitDecimals(outW + pad * 2);
3637
+ const svgOutH = isSafari() && wantsSize ? vbH : limitDecimals(outH + pad * 2);
3377
3638
  const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
3378
3639
  const svgFooter = "</svg>";
3379
3640
  svgString = svgHeader + foString + svgFooter;
3380
3641
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
3642
+ state = { svgString, dataURL, ...state };
3381
3643
  resolve();
3382
3644
  }, { fast });
3383
3645
  });
3646
+ await runHook("afterRender", state);
3384
3647
  const sandbox = document.getElementById("snapdom-sandbox");
3385
3648
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
3386
- return dataURL;
3649
+ return state.dataURL;
3650
+ }
3651
+ var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
3652
+ function parseFilterDropShadows(cs) {
3653
+ const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
3654
+ if (!raw || raw === "none") {
3655
+ return { bleed: { top: 0, right: 0, bottom: 0, left: 0 }, has: false };
3656
+ }
3657
+ const tokens = raw.match(/drop-shadow\((?:[^()]|\([^()]*\))*\)/gi) || [];
3658
+ let t = 0, r = 0, b = 0, l = 0;
3659
+ let found = false;
3660
+ for (const tok of tokens) {
3661
+ found = true;
3662
+ const nums = tok.match(/-?\d+(?:\.\d+)?px/gi)?.map((v) => parseFloat(v)) || [];
3663
+ const [ox = 0, oy = 0, blur = 0] = nums;
3664
+ const extX = Math.abs(ox) + blur;
3665
+ const extY = Math.abs(oy) + blur;
3666
+ r = Math.max(r, extX + Math.max(ox, 0));
3667
+ l = Math.max(l, extX + Math.max(-ox, 0));
3668
+ b = Math.max(b, extY + Math.max(oy, 0));
3669
+ t = Math.max(t, extY + Math.max(-oy, 0));
3670
+ }
3671
+ return {
3672
+ bleed: {
3673
+ top: limitDecimals(t),
3674
+ right: limitDecimals(r),
3675
+ bottom: limitDecimals(b),
3676
+ left: limitDecimals(l)
3677
+ },
3678
+ has: found
3679
+ };
3387
3680
  }
3388
3681
  function normalizeRootTransforms(originalEl, cloneRoot) {
3389
3682
  if (!originalEl || !cloneRoot || !cloneRoot.style) return null;
@@ -3634,7 +3927,8 @@ function normalizeCachePolicy(v) {
3634
3927
  return "soft";
3635
3928
  }
3636
3929
  function createContext(options = {}) {
3637
- const resolvedFormat = options.format ?? "png";
3930
+ let resolvedFormat = options.format ?? "png";
3931
+ if (resolvedFormat === "jpg") resolvedFormat = "jpeg";
3638
3932
  const cachePolicy = normalizeCachePolicy(options.cache);
3639
3933
  return {
3640
3934
  // Debug & perf
@@ -3666,7 +3960,7 @@ function createContext(options = {}) {
3666
3960
  type: options.type ?? "svg",
3667
3961
  quality: options.quality ?? 0.92,
3668
3962
  dpr: options.dpr ?? (window.devicePixelRatio || 1),
3669
- backgroundColor: options.backgroundColor ?? (["jpg", "jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
3963
+ backgroundColor: options.backgroundColor ?? (["jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
3670
3964
  filename: options.filename ?? "snapDOM",
3671
3965
  // NEW flags (user-friendly)
3672
3966
  straighten: options.straighten ?? false,
@@ -3797,20 +4091,20 @@ async function toCanvas(url, options) {
3797
4091
  } else if (hasW) {
3798
4092
  const k = optW / Math.max(1, refW);
3799
4093
  outW = optW;
3800
- outH = Math.round(refH * k);
4094
+ outH = refH * k;
3801
4095
  } else if (hasH) {
3802
4096
  const k = optH / Math.max(1, refH);
3803
4097
  outH = optH;
3804
- outW = Math.round(refW * k);
4098
+ outW = refW * k;
3805
4099
  } else {
3806
4100
  outW = natW;
3807
4101
  outH = natH;
3808
4102
  }
3809
- outW = Math.round(outW * scale);
3810
- outH = Math.round(outH * scale);
4103
+ outW = outW * scale;
4104
+ outH = outH * scale;
3811
4105
  const canvas = document.createElement("canvas");
3812
- canvas.width = Math.ceil(outW * dpr);
3813
- canvas.height = Math.ceil(outH * dpr);
4106
+ canvas.width = outW * dpr;
4107
+ canvas.height = outH * dpr;
3814
4108
  canvas.style.width = `${outW}px`;
3815
4109
  canvas.style.height = `${outH}px`;
3816
4110
  const ctx = canvas.getContext("2d");
@@ -3919,16 +4213,22 @@ async function download(url, options) {
3919
4213
  }
3920
4214
 
3921
4215
  // src/api/snapdom.js
4216
+ snapdom.plugins = (...defs) => {
4217
+ registerPlugins(...defs);
4218
+ return snapdom;
4219
+ };
3922
4220
  var INTERNAL_TOKEN = Symbol("snapdom.internal");
4221
+ var INTERNAL_EXPORT_TOKEN = Symbol("snapdom.internal.silent");
3923
4222
  var _safariWarmup = false;
3924
4223
  async function snapdom(element, userOptions) {
3925
4224
  if (!element) throw new Error("Element cannot be null or undefined");
3926
4225
  const context = createContext(userOptions);
4226
+ attachSessionPlugins(context, userOptions && userOptions.plugins);
3927
4227
  if (isSafari() && (context.embedFonts === true || hasBackgroundOrMask(element))) {
3928
4228
  for (let i = 0; i < 3; i++) {
3929
4229
  try {
3930
4230
  await safariWarmup(element, userOptions);
3931
- console.log("Iteraci\xF3n n\xFAmero:", i);
4231
+ console.log("safariWarmup:", i);
3932
4232
  _safariWarmup = false;
3933
4233
  } catch {
3934
4234
  }
@@ -3943,31 +4243,90 @@ async function snapdom(element, userOptions) {
3943
4243
  }
3944
4244
  return snapdom.capture(element, context, INTERNAL_TOKEN);
3945
4245
  }
4246
+ snapdom.plugins = (...defs) => {
4247
+ registerPlugins(...defs);
4248
+ return snapdom;
4249
+ };
3946
4250
  snapdom.capture = async (el, context, _token) => {
3947
4251
  if (_token !== INTERNAL_TOKEN) throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
3948
4252
  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);
4253
+ const coreExports = {
4254
+ img: async (ctx, opts) => toImg(url, { ...ctx, ...opts || {} }),
4255
+ svg: async (ctx, opts) => toImg(url, { ...ctx, ...opts || {} }),
4256
+ canvas: async (ctx, opts) => toCanvas(url, { ...ctx, ...opts || {} }),
4257
+ blob: async (ctx, opts) => toBlob(url, { ...ctx, ...opts || {} }),
4258
+ png: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "png" }),
4259
+ jpeg: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "jpeg" }),
4260
+ webp: async (ctx, opts) => rasterize(url, { ...ctx, ...opts || {}, format: "webp" }),
4261
+ download: async (ctx, opts) => download(url, { ...ctx, ...opts || {} })
3958
4262
  };
3959
- return {
4263
+ const _pluginExports = {
4264
+ svg: (opts) => toImg(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4265
+ canvas: (opts) => toCanvas(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4266
+ png: (opts) => rasterize(url, { ...context, ...opts || {}, format: "png", [INTERNAL_EXPORT_TOKEN]: true }),
4267
+ jpeg: (opts) => rasterize(url, { ...context, ...opts || {}, format: "jpeg", [INTERNAL_EXPORT_TOKEN]: true }),
4268
+ jpg: (opts) => rasterize(url, { ...context, ...opts || {}, format: "jpeg", [INTERNAL_EXPORT_TOKEN]: true }),
4269
+ webp: (opts) => rasterize(url, { ...context, ...opts || {}, format: "webp", [INTERNAL_EXPORT_TOKEN]: true }),
4270
+ blob: (opts) => toBlob(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true }),
4271
+ img: (opts) => toImg(url, { ...context, ...opts || {}, [INTERNAL_EXPORT_TOKEN]: true })
4272
+ };
4273
+ const _defineCtx = { ...context, export: { url }, exports: _pluginExports };
4274
+ const providedMaps = await runAll("defineExports", _defineCtx);
4275
+ const provided = Object.assign({}, ...providedMaps.filter((x) => x && typeof x === "object"));
4276
+ const exportsMap = { ...coreExports, ...provided };
4277
+ if (exportsMap.jpeg && !exportsMap.jpg) {
4278
+ exportsMap.jpg = (ctx, opts) => exportsMap.jpeg(ctx, opts);
4279
+ }
4280
+ function normalizeExportOptions(type, opts) {
4281
+ const next = { ...context, ...opts || {} };
4282
+ if (type === "jpeg" || type === "jpg") {
4283
+ const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
4284
+ if (noBg) next.backgroundColor = "#ffffff";
4285
+ }
4286
+ return next;
4287
+ }
4288
+ let afterSnapFired = false;
4289
+ let _exportQueue = Promise.resolve();
4290
+ async function runExport(type, opts) {
4291
+ const job = async () => {
4292
+ const work = exportsMap[type];
4293
+ if (!work) throw new Error(`[snapdom] Unknown export type: ${type}`);
4294
+ const nextOpts = normalizeExportOptions(type, opts);
4295
+ const ctx = { ...context, export: { type, options: nextOpts, url } };
4296
+ await runHook("beforeExport", ctx);
4297
+ const result2 = await work(ctx, nextOpts);
4298
+ await runHook("afterExport", ctx, result2);
4299
+ if (!afterSnapFired) {
4300
+ afterSnapFired = true;
4301
+ await runHook("afterSnap", context);
4302
+ }
4303
+ return result2;
4304
+ };
4305
+ return _exportQueue = _exportQueue.then(job);
4306
+ }
4307
+ const result = {
3960
4308
  url,
3961
4309
  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))
4310
+ to: (type, opts) => runExport(type, opts),
4311
+ // Métodos “clásicos” que los tests esperan:
4312
+ toImg: (opts) => runExport("img", opts),
4313
+ toSvg: (opts) => runExport("svg", opts),
4314
+ toCanvas: (opts) => runExport("canvas", opts),
4315
+ toBlob: (opts) => runExport("blob", opts),
4316
+ toPng: (opts) => runExport("png", opts),
4317
+ toJpg: (opts) => runExport("jpg", opts),
4318
+ // ← alias requerido por los tests
4319
+ toWebp: (opts) => runExport("webp", opts),
4320
+ download: (opts) => runExport("download", opts)
4321
+ // ← método directo (no toDownload)
3970
4322
  };
4323
+ for (const key of Object.keys(exportsMap)) {
4324
+ const helper = "to" + key.charAt(0).toUpperCase() + key.slice(1);
4325
+ if (!result[helper]) {
4326
+ result[helper] = (opts) => runExport(key, opts);
4327
+ }
4328
+ }
4329
+ return result;
3971
4330
  };
3972
4331
  snapdom.toRaw = (el, options) => snapdom(el, options).then((result) => result.toRaw());
3973
4332
  snapdom.toImg = (el, options) => snapdom(el, options).then((result) => result.toImg());
@@ -3990,30 +4349,47 @@ async function safariWarmup(element, baseOptions) {
3990
4349
  try {
3991
4350
  url = await captureDOM(element, preflight);
3992
4351
  } catch {
3993
- return;
3994
4352
  }
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();
4353
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
4354
+ if (url) {
4355
+ await new Promise((resolve) => {
4356
+ const img = new Image();
4357
+ try {
4358
+ img.decoding = "sync";
4359
+ img.loading = "eager";
4360
+ } catch {
4361
+ }
4362
+ img.style.cssText = "position:fixed;left:0px;top:0px;width:10px;height:10px;opacity:0.01;pointer-events:none;";
4363
+ img.src = url;
4364
+ document.body.appendChild(img);
4365
+ (async () => {
4366
+ try {
4367
+ if (typeof img.decode === "function") await img.decode();
4368
+ } catch {
4369
+ }
4370
+ const start = performance.now();
4371
+ while (!(img.complete && img.naturalWidth > 0) && performance.now() - start < 900) {
4372
+ await new Promise((r) => setTimeout(r, 50));
4373
+ }
4374
+ await new Promise((r) => requestAnimationFrame(r));
4375
+ try {
4376
+ img.remove();
4377
+ } catch {
4378
+ }
4379
+ resolve();
4380
+ })();
4381
+ });
4382
+ }
4383
+ element.querySelectorAll("canvas").forEach((c) => {
4384
+ try {
4385
+ const ctx = c.getContext("2d", { willReadFrequently: true });
4386
+ if (ctx) {
4387
+ ctx.getImageData(0, 0, 1, 1);
4388
+ }
4389
+ } catch {
4390
+ }
4016
4391
  });
4392
+ _safariWarmup = true;
4017
4393
  }
4018
4394
  function hasBackgroundOrMask(el) {
4019
4395
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_ELEMENT);
@@ -4026,6 +4402,7 @@ function hasBackgroundOrMask(el) {
4026
4402
  const bg = cs.backgroundImage && cs.backgroundImage !== "none";
4027
4403
  const mask = cs.maskImage && cs.maskImage !== "none" || cs.webkitMaskImage && cs.webkitMaskImage !== "none";
4028
4404
  if (bg || mask) return true;
4405
+ if (node.tagName === "CANVAS") return true;
4029
4406
  }
4030
4407
  return false;
4031
4408
  }
@@ -4051,6 +4428,7 @@ async function preCache(root = document, options = {}) {
4051
4428
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
4052
4429
  }
4053
4430
  cache.image = cache.image || /* @__PURE__ */ new Map();
4431
+ cache.background = cache.background || /* @__PURE__ */ new Map();
4054
4432
  try {
4055
4433
  await inlineBackgroundImages(
4056
4434
  root,
@@ -4063,7 +4441,13 @@ async function preCache(root = document, options = {}) {
4063
4441
  }
4064
4442
  let imgEls = [], allEls = [];
4065
4443
  try {
4066
- if (root?.querySelectorAll) {
4444
+ if (root && root.nodeType === 1) {
4445
+ const descendants = root.querySelectorAll ? Array.from(root.querySelectorAll("*")) : [];
4446
+ allEls = [root, ...descendants];
4447
+ imgEls = [];
4448
+ if (root.tagName === "IMG" && root.getAttribute("src")) imgEls.push(root);
4449
+ imgEls.push(...Array.from(root.querySelectorAll?.("img[src]") || []));
4450
+ } else if (root?.querySelectorAll) {
4067
4451
  imgEls = Array.from(root.querySelectorAll("img[src]"));
4068
4452
  allEls = Array.from(root.querySelectorAll("*"));
4069
4453
  }
@@ -4087,17 +4471,18 @@ async function preCache(root = document, options = {}) {
4087
4471
  for (const el of allEls) {
4088
4472
  let bg = "";
4089
4473
  try {
4090
- bg = getStyle(el).backgroundImage;
4474
+ bg = el?.style?.backgroundImage || "";
4475
+ if (!bg || bg === "none") {
4476
+ bg = getStyle(el).backgroundImage;
4477
+ }
4091
4478
  } catch {
4092
4479
  }
4093
4480
  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
- }
4481
+ const urlEntries = bg.match(/url\((?:[^()"']+|"(?:[^"]*)"|'(?:[^']*)')\)/gi) || [];
4482
+ for (const entry of urlEntries) {
4483
+ const p = Promise.resolve().then(() => inlineSingleBackgroundEntry(entry, { ...options, useProxy })).catch(() => {
4484
+ });
4485
+ promises.push(p);
4101
4486
  }
4102
4487
  }
4103
4488
  }