@zumer/snapdom 1.9.10-dev.1 → 1.9.10
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/README.md +94 -43
- package/dist/snapdom.js +614 -197
- package/dist/snapdom.min.js +4 -4
- package/dist/snapdom.min.mjs +5 -5
- package/dist/snapdom.mjs +465 -58
- package/package.json +1 -1
- package/types/snapdom.d.ts +108 -50
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.10
|
|
4
|
+
* v.1.9.10
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -42,10 +42,13 @@ function applyCachePolicy(policy = "soft") {
|
|
|
42
42
|
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
43
43
|
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
44
44
|
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
45
|
-
cache.
|
|
46
|
-
cache.
|
|
47
|
-
cache.
|
|
48
|
-
cache.
|
|
45
|
+
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
cache.baseStyle = /* @__PURE__ */ new Map();
|
|
47
|
+
cache.defaultStyle = /* @__PURE__ */ new Map();
|
|
48
|
+
cache.image = /* @__PURE__ */ new Map();
|
|
49
|
+
cache.background = /* @__PURE__ */ new Map();
|
|
50
|
+
cache.resource = /* @__PURE__ */ new Map();
|
|
51
|
+
cache.font = /* @__PURE__ */ new Set();
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
default: {
|
|
@@ -637,6 +640,11 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
637
640
|
const ctx = _resolveCtx(sessionOrCtx, opts);
|
|
638
641
|
const resetMode = ctx.options && ctx.options.cache || "auto";
|
|
639
642
|
if (resetMode !== "disabled") setupInvalidationOnce(document.documentElement);
|
|
643
|
+
if (resetMode === "disabled" && !ctx.session.__bumpedForDisabled) {
|
|
644
|
+
bumpEpoch();
|
|
645
|
+
snapshotKeyCache.clear();
|
|
646
|
+
ctx.session.__bumpedForDisabled = true;
|
|
647
|
+
}
|
|
640
648
|
if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
|
|
641
649
|
const author = source.getAttribute?.("style");
|
|
642
650
|
if (author) clone.setAttribute("style", author);
|
|
@@ -658,6 +666,31 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
658
666
|
}
|
|
659
667
|
|
|
660
668
|
// src/core/clone.js
|
|
669
|
+
function idleCallback(childList, callback, fast) {
|
|
670
|
+
return Promise.all(childList.map((child) => {
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
function deal() {
|
|
673
|
+
idle(
|
|
674
|
+
(deadline) => {
|
|
675
|
+
if (deadline instanceof IdleDeadline) {
|
|
676
|
+
if (deadline.timeRemaining()) {
|
|
677
|
+
callback(child, resolve);
|
|
678
|
+
} else {
|
|
679
|
+
deal();
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
callback(child, resolve);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
fast
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
deal();
|
|
691
|
+
});
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
661
694
|
function addNotSlottedRightmost(sel) {
|
|
662
695
|
sel = sel.trim();
|
|
663
696
|
if (!sel) return sel;
|
|
@@ -779,10 +812,84 @@ function markSlottedSubtree(root) {
|
|
|
779
812
|
root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
|
|
780
813
|
}
|
|
781
814
|
}
|
|
782
|
-
function
|
|
815
|
+
async function getAccessibleIframeDocument(iframe, attempts = 3) {
|
|
816
|
+
const probe = () => {
|
|
817
|
+
try {
|
|
818
|
+
return iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
let doc = probe();
|
|
824
|
+
let i = 0;
|
|
825
|
+
while (i < attempts && (!doc || !doc.body && !doc.documentElement)) {
|
|
826
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
827
|
+
doc = probe();
|
|
828
|
+
i++;
|
|
829
|
+
}
|
|
830
|
+
return doc && (doc.body || doc.documentElement) ? doc : null;
|
|
831
|
+
}
|
|
832
|
+
function measureContentBox(el) {
|
|
833
|
+
const rect = el.getBoundingClientRect();
|
|
834
|
+
let bl = 0, br = 0, bt = 0, bb = 0;
|
|
835
|
+
try {
|
|
836
|
+
const cs = getComputedStyle(el);
|
|
837
|
+
bl = parseFloat(cs.borderLeftWidth) || 0;
|
|
838
|
+
br = parseFloat(cs.borderRightWidth) || 0;
|
|
839
|
+
bt = parseFloat(cs.borderTopWidth) || 0;
|
|
840
|
+
bb = parseFloat(cs.borderBottomWidth) || 0;
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
const contentWidth = Math.max(0, Math.round(rect.width - (bl + br)));
|
|
844
|
+
const contentHeight = Math.max(0, Math.round(rect.height - (bt + bb)));
|
|
845
|
+
return { contentWidth, contentHeight, rect };
|
|
846
|
+
}
|
|
847
|
+
function pinIframeViewport(doc, w, h) {
|
|
848
|
+
const style = doc.createElement("style");
|
|
849
|
+
style.setAttribute("data-sd-iframe-pin", "");
|
|
850
|
+
style.textContent = `html, body {margin: 0 !important;padding: 0 !important;width: ${w}px !important;height: ${h}px !important;min-width: ${w}px !important;min-height: ${h}px !important;box-sizing: border-box !important;overflow: hidden !important;background-clip: border-box !important;}`;
|
|
851
|
+
(doc.head || doc.documentElement).appendChild(style);
|
|
852
|
+
return () => {
|
|
853
|
+
try {
|
|
854
|
+
style.remove();
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function rasterizeIframe(iframe, sessionCache, options) {
|
|
860
|
+
const doc = await getAccessibleIframeDocument(iframe, 3);
|
|
861
|
+
if (!doc) throw new Error("iframe document not accessible/ready");
|
|
862
|
+
const { contentWidth, contentHeight, rect } = measureContentBox(iframe);
|
|
863
|
+
const snap = options?.snap;
|
|
864
|
+
if (!snap || typeof snap.toPng !== "function") {
|
|
865
|
+
throw new Error("snapdom.toPng not available in iframe or window");
|
|
866
|
+
}
|
|
867
|
+
const nested = { ...options, scale: 1 };
|
|
868
|
+
const unpin = pinIframeViewport(doc, contentWidth, contentHeight);
|
|
869
|
+
let imgEl;
|
|
870
|
+
try {
|
|
871
|
+
imgEl = await snap.toPng(doc.documentElement, nested);
|
|
872
|
+
} finally {
|
|
873
|
+
unpin();
|
|
874
|
+
}
|
|
875
|
+
imgEl.style.display = "block";
|
|
876
|
+
imgEl.style.width = `${contentWidth}px`;
|
|
877
|
+
imgEl.style.height = `${contentHeight}px`;
|
|
878
|
+
const wrapper = document.createElement("div");
|
|
879
|
+
sessionCache.nodeMap.set(wrapper, iframe);
|
|
880
|
+
inlineAllStyles(iframe, wrapper, sessionCache, options);
|
|
881
|
+
wrapper.style.overflow = "hidden";
|
|
882
|
+
wrapper.style.display = "block";
|
|
883
|
+
if (!wrapper.style.width) wrapper.style.width = `${Math.round(rect.width)}px`;
|
|
884
|
+
if (!wrapper.style.height) wrapper.style.height = `${Math.round(rect.height)}px`;
|
|
885
|
+
wrapper.appendChild(imgEl);
|
|
886
|
+
return wrapper;
|
|
887
|
+
}
|
|
888
|
+
async function deepClone(node, sessionCache, options) {
|
|
783
889
|
if (!node) throw new Error("Invalid node");
|
|
784
890
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
785
891
|
let pendingSelectValue = null;
|
|
892
|
+
let pendingTextAreaValue = null;
|
|
786
893
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
787
894
|
const tag = (node.localName || node.tagName || "").toLowerCase();
|
|
788
895
|
if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
|
|
@@ -831,6 +938,20 @@ function deepClone(node, sessionCache, options) {
|
|
|
831
938
|
}
|
|
832
939
|
}
|
|
833
940
|
if (node.tagName === "IFRAME") {
|
|
941
|
+
let sameOrigin = false;
|
|
942
|
+
try {
|
|
943
|
+
sameOrigin = !!(node.contentDocument || node.contentWindow?.document);
|
|
944
|
+
} catch {
|
|
945
|
+
sameOrigin = false;
|
|
946
|
+
}
|
|
947
|
+
if (sameOrigin) {
|
|
948
|
+
try {
|
|
949
|
+
const wrapper = await rasterizeIframe(node, sessionCache, options);
|
|
950
|
+
return wrapper;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
console.warn("[SnapDOM] iframe rasterization failed, fallback:", err);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
834
955
|
if (options.placeholders) {
|
|
835
956
|
const fallback = document.createElement("div");
|
|
836
957
|
fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
|
|
@@ -870,14 +991,31 @@ function deepClone(node, sessionCache, options) {
|
|
|
870
991
|
sessionCache.nodeMap.set(clone, node);
|
|
871
992
|
if (node.tagName === "IMG") {
|
|
872
993
|
freezeImgSrcset(node, clone);
|
|
994
|
+
try {
|
|
995
|
+
const rect = node.getBoundingClientRect();
|
|
996
|
+
let w = Math.round(rect.width || 0);
|
|
997
|
+
let h = Math.round(rect.height || 0);
|
|
998
|
+
if (!w || !h) {
|
|
999
|
+
const computed = window.getComputedStyle(node);
|
|
1000
|
+
const cssW = parseFloat(computed.width) || 0;
|
|
1001
|
+
const cssH = parseFloat(computed.height) || 0;
|
|
1002
|
+
const attrW = parseInt(node.getAttribute("width") || "", 10) || 0;
|
|
1003
|
+
const attrH = parseInt(node.getAttribute("height") || "", 10) || 0;
|
|
1004
|
+
const propW = node.width || node.naturalWidth || 0;
|
|
1005
|
+
const propH = node.height || node.naturalHeight || 0;
|
|
1006
|
+
w = Math.round(w || cssW || attrW || propW || 0);
|
|
1007
|
+
h = Math.round(h || cssH || attrH || propH || 0);
|
|
1008
|
+
}
|
|
1009
|
+
if (w) clone.dataset.snapdomWidth = String(w);
|
|
1010
|
+
if (h) clone.dataset.snapdomHeight = String(h);
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
873
1013
|
}
|
|
874
1014
|
} catch (err) {
|
|
875
1015
|
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
876
1016
|
throw err;
|
|
877
1017
|
}
|
|
878
1018
|
if (node instanceof HTMLTextAreaElement) {
|
|
879
|
-
clone.textContent = node.value;
|
|
880
|
-
clone.value = node.value;
|
|
881
1019
|
const rect = node.getBoundingClientRect();
|
|
882
1020
|
clone.style.width = `${rect.width}px`;
|
|
883
1021
|
clone.style.height = `${rect.height}px`;
|
|
@@ -894,8 +1032,22 @@ function deepClone(node, sessionCache, options) {
|
|
|
894
1032
|
if (node instanceof HTMLSelectElement) {
|
|
895
1033
|
pendingSelectValue = node.value;
|
|
896
1034
|
}
|
|
1035
|
+
if (node instanceof HTMLTextAreaElement) {
|
|
1036
|
+
pendingTextAreaValue = node.value;
|
|
1037
|
+
}
|
|
897
1038
|
inlineAllStyles(node, clone, sessionCache, options);
|
|
898
1039
|
if (node.shadowRoot) {
|
|
1040
|
+
let callback2 = function(child, resolve) {
|
|
1041
|
+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
1042
|
+
return resolve(null);
|
|
1043
|
+
} else {
|
|
1044
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1045
|
+
resolve(clonedChild || null);
|
|
1046
|
+
}).catch((e) => {
|
|
1047
|
+
resolve(null);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
899
1051
|
try {
|
|
900
1052
|
const slots = node.shadowRoot.querySelectorAll("slot");
|
|
901
1053
|
for (const s of slots) {
|
|
@@ -921,31 +1073,38 @@ function deepClone(node, sessionCache, options) {
|
|
|
921
1073
|
const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
|
|
922
1074
|
injectScopedStyle(clone, seed + rewritten, scopeId);
|
|
923
1075
|
const shadowFrag = document.createDocumentFragment();
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const clonedChild = deepClone(child, sessionCache, options);
|
|
927
|
-
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
928
|
-
}
|
|
1076
|
+
const cloneList2 = await idleCallback(Array.from(node.shadowRoot.childNodes), callback2, options.fast);
|
|
1077
|
+
shadowFrag.append(...cloneList2.filter((clonedChild) => !!clonedChild));
|
|
929
1078
|
clone.appendChild(shadowFrag);
|
|
930
1079
|
}
|
|
931
1080
|
if (node.tagName === "SLOT") {
|
|
1081
|
+
let callback2 = function(child, resolve) {
|
|
1082
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1083
|
+
if (clonedChild) {
|
|
1084
|
+
markSlottedSubtree(clonedChild);
|
|
1085
|
+
}
|
|
1086
|
+
resolve(clonedChild || null);
|
|
1087
|
+
}).catch((e) => {
|
|
1088
|
+
resolve(null);
|
|
1089
|
+
});
|
|
1090
|
+
};
|
|
932
1091
|
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
933
1092
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
934
1093
|
const fragment = document.createDocumentFragment();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (clonedChild) {
|
|
938
|
-
markSlottedSubtree(clonedChild);
|
|
939
|
-
fragment.appendChild(clonedChild);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
1094
|
+
const cloneList2 = await idleCallback(Array.from(nodesToClone), callback2, options.fast);
|
|
1095
|
+
fragment.append(...cloneList2.filter((clonedChild) => !!clonedChild));
|
|
942
1096
|
return fragment;
|
|
943
1097
|
}
|
|
944
|
-
|
|
945
|
-
if (clonedAssignedNodes.has(child))
|
|
946
|
-
|
|
947
|
-
|
|
1098
|
+
function callback(child, resolve) {
|
|
1099
|
+
if (clonedAssignedNodes.has(child)) return resolve(null);
|
|
1100
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1101
|
+
resolve(clonedChild || null);
|
|
1102
|
+
}).catch((e) => {
|
|
1103
|
+
resolve(null);
|
|
1104
|
+
});
|
|
948
1105
|
}
|
|
1106
|
+
const cloneList = await idleCallback(Array.from(node.childNodes), callback, options.fast);
|
|
1107
|
+
clone.append(...cloneList.filter((clonedChild) => !!clonedChild));
|
|
949
1108
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
950
1109
|
clone.value = pendingSelectValue;
|
|
951
1110
|
for (const opt of clone.options) {
|
|
@@ -956,6 +1115,9 @@ function deepClone(node, sessionCache, options) {
|
|
|
956
1115
|
}
|
|
957
1116
|
}
|
|
958
1117
|
}
|
|
1118
|
+
if (pendingTextAreaValue !== null && clone instanceof HTMLTextAreaElement) {
|
|
1119
|
+
clone.textContent = pendingTextAreaValue;
|
|
1120
|
+
}
|
|
959
1121
|
return clone;
|
|
960
1122
|
}
|
|
961
1123
|
|
|
@@ -1826,7 +1988,7 @@ async function prepareClone(element, options = {}) {
|
|
|
1826
1988
|
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1827
1989
|
}
|
|
1828
1990
|
try {
|
|
1829
|
-
clone = deepClone(element, sessionCache, options, element);
|
|
1991
|
+
clone = await deepClone(element, sessionCache, options, element);
|
|
1830
1992
|
} catch (e) {
|
|
1831
1993
|
console.warn("deepClone failed:", e);
|
|
1832
1994
|
throw e;
|
|
@@ -2064,6 +2226,30 @@ async function inlineImages(clone, options = {}) {
|
|
|
2064
2226
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2065
2227
|
return;
|
|
2066
2228
|
}
|
|
2229
|
+
const { defaultImageUrl } = options || {};
|
|
2230
|
+
if (defaultImageUrl) {
|
|
2231
|
+
try {
|
|
2232
|
+
const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
|
|
2233
|
+
const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
|
|
2234
|
+
const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
|
|
2235
|
+
const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
|
|
2236
|
+
const styleW = parseFloat(img.style?.width || "") || 0;
|
|
2237
|
+
const styleH = parseFloat(img.style?.height || "") || 0;
|
|
2238
|
+
const width = dsW || styleW || attrW || img.width || void 0;
|
|
2239
|
+
const height = dsH || styleH || attrH || img.height || void 0;
|
|
2240
|
+
const fallbackUrl = typeof defaultImageUrl === "function" ? await defaultImageUrl({ width, height, src, element: img }) : defaultImageUrl;
|
|
2241
|
+
if (fallbackUrl) {
|
|
2242
|
+
const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
|
|
2243
|
+
img.src = fallbackData.data;
|
|
2244
|
+
if (!img.width && width) img.width = width;
|
|
2245
|
+
if (!img.height && height) img.height = height;
|
|
2246
|
+
if (!img.width) img.width = img.naturalWidth || 100;
|
|
2247
|
+
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2067
2253
|
const w = img.width || img.naturalWidth || 100;
|
|
2068
2254
|
const h = img.height || img.naturalHeight || 100;
|
|
2069
2255
|
if (options.placeholders !== false) {
|
|
@@ -2096,17 +2282,40 @@ async function inlineImages(clone, options = {}) {
|
|
|
2096
2282
|
// src/modules/background.js
|
|
2097
2283
|
async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
2098
2284
|
const queue = [[source, clone]];
|
|
2099
|
-
const
|
|
2285
|
+
const URL_PROPS = [
|
|
2100
2286
|
"background-image",
|
|
2287
|
+
// Mask shorthands & images (both standard and WebKit)
|
|
2101
2288
|
"mask",
|
|
2102
2289
|
"mask-image",
|
|
2290
|
+
"-webkit-mask",
|
|
2103
2291
|
"-webkit-mask-image",
|
|
2292
|
+
// Mask sources (rare, but keep)
|
|
2104
2293
|
"mask-source",
|
|
2105
2294
|
"mask-box-image-source",
|
|
2106
2295
|
"mask-border-source",
|
|
2107
2296
|
"-webkit-mask-box-image-source",
|
|
2297
|
+
// Border image
|
|
2108
2298
|
"border-image",
|
|
2109
|
-
"border-image-source"
|
|
2299
|
+
"border-image-source"
|
|
2300
|
+
];
|
|
2301
|
+
const MASK_LAYOUT_PROPS = [
|
|
2302
|
+
"mask-position",
|
|
2303
|
+
"mask-size",
|
|
2304
|
+
"mask-repeat",
|
|
2305
|
+
// WebKit variants
|
|
2306
|
+
"-webkit-mask-position",
|
|
2307
|
+
"-webkit-mask-size",
|
|
2308
|
+
"-webkit-mask-repeat",
|
|
2309
|
+
// Extra (optional but helpful across engines)
|
|
2310
|
+
"mask-origin",
|
|
2311
|
+
"mask-clip",
|
|
2312
|
+
"-webkit-mask-origin",
|
|
2313
|
+
"-webkit-mask-clip",
|
|
2314
|
+
// Some engines expose X/Y position separately:
|
|
2315
|
+
"-webkit-mask-position-x",
|
|
2316
|
+
"-webkit-mask-position-y"
|
|
2317
|
+
];
|
|
2318
|
+
const BORDER_AUX_PROPS = [
|
|
2110
2319
|
"border-image-slice",
|
|
2111
2320
|
"border-image-width",
|
|
2112
2321
|
"border-image-outset",
|
|
@@ -2121,10 +2330,7 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
2121
2330
|
const bis = style.getPropertyValue("border-image-source");
|
|
2122
2331
|
return bi && bi !== "none" || bis && bis !== "none";
|
|
2123
2332
|
})();
|
|
2124
|
-
for (const prop of
|
|
2125
|
-
if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
|
|
2126
|
-
continue;
|
|
2127
|
-
}
|
|
2333
|
+
for (const prop of URL_PROPS) {
|
|
2128
2334
|
const val = style.getPropertyValue(prop);
|
|
2129
2335
|
if (!val || val === "none") continue;
|
|
2130
2336
|
const splits = splitBackgroundImage(val);
|
|
@@ -2135,6 +2341,18 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
2135
2341
|
cloneNode.style.setProperty(prop, inlined.join(", "));
|
|
2136
2342
|
}
|
|
2137
2343
|
}
|
|
2344
|
+
for (const prop of MASK_LAYOUT_PROPS) {
|
|
2345
|
+
const val = style.getPropertyValue(prop);
|
|
2346
|
+
if (!val || val === "initial") continue;
|
|
2347
|
+
cloneNode.style.setProperty(prop, val);
|
|
2348
|
+
}
|
|
2349
|
+
if (hasBorderImage) {
|
|
2350
|
+
for (const prop of BORDER_AUX_PROPS) {
|
|
2351
|
+
const val = style.getPropertyValue(prop);
|
|
2352
|
+
if (!val || val === "initial") continue;
|
|
2353
|
+
cloneNode.style.setProperty(prop, val);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2138
2356
|
const sChildren = Array.from(srcNode.children);
|
|
2139
2357
|
const cChildren = Array.from(cloneNode.children);
|
|
2140
2358
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
@@ -2177,7 +2395,13 @@ async function captureDOM(element, options) {
|
|
|
2177
2395
|
);
|
|
2178
2396
|
await ensureFontsReady(families, 2);
|
|
2179
2397
|
}
|
|
2180
|
-
fontsCSS = await embedCustomFonts({
|
|
2398
|
+
fontsCSS = await embedCustomFonts({
|
|
2399
|
+
required,
|
|
2400
|
+
usedCodepoints,
|
|
2401
|
+
preCached: false,
|
|
2402
|
+
exclude: options.excludeFonts,
|
|
2403
|
+
useProxy: options.useProxy
|
|
2404
|
+
});
|
|
2181
2405
|
resolve();
|
|
2182
2406
|
}, { fast });
|
|
2183
2407
|
});
|
|
@@ -2197,12 +2421,13 @@ async function captureDOM(element, options) {
|
|
|
2197
2421
|
}
|
|
2198
2422
|
await new Promise((resolve) => {
|
|
2199
2423
|
idle(() => {
|
|
2200
|
-
const rect = element
|
|
2424
|
+
const rect = getNaturalBorderBoxSize(element);
|
|
2201
2425
|
let w = rect.width;
|
|
2202
2426
|
let h = rect.height;
|
|
2203
2427
|
const hasW = Number.isFinite(options.width);
|
|
2204
2428
|
const hasH = Number.isFinite(options.height);
|
|
2205
|
-
const
|
|
2429
|
+
const scaleOpt = options.scale ?? 1;
|
|
2430
|
+
const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
|
|
2206
2431
|
if (!hasScale) {
|
|
2207
2432
|
const aspect = rect.width / rect.height;
|
|
2208
2433
|
if (hasW && hasH) {
|
|
@@ -2216,30 +2441,79 @@ async function captureDOM(element, options) {
|
|
|
2216
2441
|
w = h * aspect;
|
|
2217
2442
|
}
|
|
2218
2443
|
}
|
|
2219
|
-
w = Math.ceil(w);
|
|
2220
|
-
h = Math.ceil(h);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2444
|
+
w = Math.max(1, Math.ceil(w));
|
|
2445
|
+
h = Math.max(1, Math.ceil(h));
|
|
2446
|
+
const csEl = getComputedStyle(element);
|
|
2447
|
+
const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
|
|
2448
|
+
const ind = readIndividualTransforms(element);
|
|
2449
|
+
let outW, outH, tx, ty, cancelTranslateX = 0, cancelTranslateY = 0;
|
|
2450
|
+
if (!hasBBoxAffectingTransform(element)) {
|
|
2451
|
+
const r = element.getBoundingClientRect();
|
|
2452
|
+
const vp = inflateRect(r, { top: 0, right: 0, bottom: 0, left: 0 });
|
|
2453
|
+
outW = Math.max(1, vp.width);
|
|
2454
|
+
outH = Math.max(1, vp.height);
|
|
2455
|
+
tx = -(vp.x - Math.floor(r.left));
|
|
2456
|
+
ty = -(vp.y - Math.floor(r.top));
|
|
2457
|
+
} else {
|
|
2458
|
+
const TOTAL = readTotalTransformMatrix({
|
|
2459
|
+
baseTransform,
|
|
2460
|
+
rotate: ind.rotate || "0deg",
|
|
2461
|
+
scale: ind.scale,
|
|
2462
|
+
translate: ind.translate
|
|
2463
|
+
});
|
|
2464
|
+
const e = TOTAL.e ?? TOTAL.m41 ?? 0;
|
|
2465
|
+
const f = TOTAL.f ?? TOTAL.m42 ?? 0;
|
|
2466
|
+
cancelTranslateX = -e;
|
|
2467
|
+
cancelTranslateY = -f;
|
|
2468
|
+
const M2D = TOTAL.is2D ? matrix2DNoTranslate(TOTAL) : matrix2DNoTranslate(new DOMMatrix(TOTAL.toString()));
|
|
2469
|
+
const bb = bboxFromMatrix(w, h, M2D);
|
|
2470
|
+
outW = Math.max(1, Math.ceil(bb.width));
|
|
2471
|
+
outH = Math.max(1, Math.ceil(bb.height));
|
|
2472
|
+
tx = -bb.minX;
|
|
2473
|
+
ty = -bb.minY;
|
|
2231
2474
|
}
|
|
2232
2475
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
2233
2476
|
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
2234
2477
|
fo.setAttribute("width", "100%");
|
|
2235
2478
|
fo.setAttribute("height", "100%");
|
|
2479
|
+
fo.setAttribute("x", String(tx));
|
|
2480
|
+
fo.setAttribute("y", String(ty));
|
|
2481
|
+
fo.style.overflow = "visible";
|
|
2236
2482
|
const styleTag = document.createElement("style");
|
|
2237
|
-
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
|
|
2483
|
+
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
|
|
2238
2484
|
fo.appendChild(styleTag);
|
|
2239
|
-
|
|
2485
|
+
const container = document.createElement("div");
|
|
2486
|
+
container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2487
|
+
container.style.width = `${w}px`;
|
|
2488
|
+
container.style.height = `${h}px`;
|
|
2489
|
+
container.style.overflow = "visible";
|
|
2490
|
+
container.style.transformOrigin = "0 0";
|
|
2491
|
+
const cancels = [];
|
|
2492
|
+
if (cancelTranslateX || cancelTranslateY) {
|
|
2493
|
+
cancels.push(`translate(${cancelTranslateX}px, ${cancelTranslateY}px)`);
|
|
2494
|
+
}
|
|
2495
|
+
if (!hasScale && (hasW || hasH)) {
|
|
2496
|
+
const sxWH = rect.width ? w / rect.width : 1;
|
|
2497
|
+
const syWH = rect.height ? h / rect.height : 1;
|
|
2498
|
+
cancels.push(`scale(${sxWH}, ${syWH})`);
|
|
2499
|
+
} else if (hasScale) {
|
|
2500
|
+
cancels.push(`scale(${scaleOpt})`);
|
|
2501
|
+
}
|
|
2502
|
+
if (cancels.length) container.style.transform = cancels.join(" ");
|
|
2503
|
+
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2504
|
+
clone.style.transformOrigin = "0 0";
|
|
2505
|
+
clone.style.position = "static";
|
|
2506
|
+
clone.style.left = "0";
|
|
2507
|
+
clone.style.top = "0";
|
|
2508
|
+
if (baseTransform) clone.style.transform = baseTransform;
|
|
2509
|
+
clone.style.rotate = ind.rotate || "0deg";
|
|
2510
|
+
clone.style.scale = ind.scale || "1";
|
|
2511
|
+
clone.style.translate = ind.translate || "0 0";
|
|
2512
|
+
container.appendChild(clone);
|
|
2513
|
+
fo.appendChild(container);
|
|
2240
2514
|
const serializer = new XMLSerializer();
|
|
2241
2515
|
const foString = serializer.serializeToString(fo);
|
|
2242
|
-
const svgHeader = `<svg xmlns="${svgNS}" width="${
|
|
2516
|
+
const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
|
|
2243
2517
|
const svgFooter = "</svg>";
|
|
2244
2518
|
svgString = svgHeader + foString + svgFooter;
|
|
2245
2519
|
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
@@ -2250,11 +2524,131 @@ async function captureDOM(element, options) {
|
|
|
2250
2524
|
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
2251
2525
|
return dataURL;
|
|
2252
2526
|
}
|
|
2527
|
+
function getNaturalBorderBoxSize(el) {
|
|
2528
|
+
const cs = getComputedStyle(el);
|
|
2529
|
+
const px = (v) => parseFloat(v) || 0;
|
|
2530
|
+
let w = px(cs.width);
|
|
2531
|
+
let h = px(cs.height);
|
|
2532
|
+
if (cs.boxSizing === "content-box") {
|
|
2533
|
+
w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
|
|
2534
|
+
h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
2535
|
+
}
|
|
2536
|
+
return { width: w, height: h };
|
|
2537
|
+
}
|
|
2538
|
+
function matrixFromComputed(el) {
|
|
2539
|
+
const tr = getComputedStyle(el).transform;
|
|
2540
|
+
if (!tr || tr === "none") return new DOMMatrix();
|
|
2541
|
+
try {
|
|
2542
|
+
return new DOMMatrix(tr);
|
|
2543
|
+
} catch {
|
|
2544
|
+
return new WebKitCSSMatrix(tr);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
function matrix2DNoTranslate(M) {
|
|
2548
|
+
return new DOMMatrix([M.a, M.b, M.c, M.d, 0, 0]);
|
|
2549
|
+
}
|
|
2550
|
+
function bboxFromMatrix(w, h, M2D) {
|
|
2551
|
+
const pts = [
|
|
2552
|
+
{ x: 0, y: 0 },
|
|
2553
|
+
{ x: w, y: 0 },
|
|
2554
|
+
{ x: 0, y: h },
|
|
2555
|
+
{ x: w, y: h }
|
|
2556
|
+
];
|
|
2557
|
+
const t = pts.map((p) => ({
|
|
2558
|
+
x: M2D.a * p.x + M2D.c * p.y,
|
|
2559
|
+
y: M2D.b * p.x + M2D.d * p.y
|
|
2560
|
+
}));
|
|
2561
|
+
const minX = Math.min(...t.map((p) => p.x));
|
|
2562
|
+
const maxX = Math.max(...t.map((p) => p.x));
|
|
2563
|
+
const minY = Math.min(...t.map((p) => p.y));
|
|
2564
|
+
const maxY = Math.max(...t.map((p) => p.y));
|
|
2565
|
+
return { minX, minY, width: maxX - minX, height: maxY - minY };
|
|
2566
|
+
}
|
|
2567
|
+
function readIndividualTransforms(el) {
|
|
2568
|
+
const out = { rotate: "0deg", scale: null, translate: null };
|
|
2569
|
+
const map = el.computedStyleMap?.();
|
|
2570
|
+
if (map) {
|
|
2571
|
+
const rot = map.get("rotate");
|
|
2572
|
+
if (rot) {
|
|
2573
|
+
const ang = rot.angle ?? rot;
|
|
2574
|
+
if (ang && "unit" in ang) {
|
|
2575
|
+
out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
|
|
2576
|
+
} else {
|
|
2577
|
+
out.rotate = String(rot);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
const sc = map.get("scale");
|
|
2581
|
+
if (sc && sc.length) {
|
|
2582
|
+
const sx = sc[0]?.value ?? 1;
|
|
2583
|
+
const sy = sc[1]?.value ?? sx;
|
|
2584
|
+
out.scale = `${sx} ${sy}`;
|
|
2585
|
+
}
|
|
2586
|
+
const tr = map.get("translate");
|
|
2587
|
+
if (tr && tr.length) {
|
|
2588
|
+
const tx = tr[0]?.value ?? 0;
|
|
2589
|
+
const ty = tr[1]?.value ?? 0;
|
|
2590
|
+
const ux = tr[0]?.unit ?? "px";
|
|
2591
|
+
const uy = tr[1]?.unit ?? "px";
|
|
2592
|
+
out.translate = `${tx}${ux} ${ty}${uy}`;
|
|
2593
|
+
}
|
|
2594
|
+
return out;
|
|
2595
|
+
}
|
|
2596
|
+
const cs = getComputedStyle(el);
|
|
2597
|
+
out.rotate = cs.rotate && cs.rotate !== "none" ? cs.rotate : "0deg";
|
|
2598
|
+
out.scale = cs.scale && cs.scale !== "none" ? cs.scale : null;
|
|
2599
|
+
out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
|
|
2600
|
+
return out;
|
|
2601
|
+
}
|
|
2602
|
+
function hasBBoxAffectingTransform(el) {
|
|
2603
|
+
const t = getComputedStyle(el).transform || "none";
|
|
2604
|
+
if (t === "none" || t === "matrix(1, 0, 0, 1, 0, 0)") return false;
|
|
2605
|
+
if (/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,/i.test(t)) return false;
|
|
2606
|
+
return true;
|
|
2607
|
+
}
|
|
2608
|
+
function inflateRect(r, p) {
|
|
2609
|
+
const x = Math.floor(r.left - p.left);
|
|
2610
|
+
const y = Math.floor(r.top - p.top);
|
|
2611
|
+
const w = Math.ceil(r.width + p.left + p.right);
|
|
2612
|
+
const h = Math.ceil(r.height + p.top + p.bottom);
|
|
2613
|
+
return { x, y, width: w, height: h };
|
|
2614
|
+
}
|
|
2615
|
+
var __measureHost = null;
|
|
2616
|
+
function getMeasureHost() {
|
|
2617
|
+
if (__measureHost) return __measureHost;
|
|
2618
|
+
const n = document.createElement("div");
|
|
2619
|
+
n.id = "snapdom-measure-slot";
|
|
2620
|
+
n.setAttribute("aria-hidden", "true");
|
|
2621
|
+
Object.assign(n.style, {
|
|
2622
|
+
position: "absolute",
|
|
2623
|
+
left: "-99999px",
|
|
2624
|
+
top: "0px",
|
|
2625
|
+
width: "0px",
|
|
2626
|
+
height: "0px",
|
|
2627
|
+
overflow: "hidden",
|
|
2628
|
+
opacity: "0",
|
|
2629
|
+
pointerEvents: "none",
|
|
2630
|
+
contain: "size layout style"
|
|
2631
|
+
});
|
|
2632
|
+
document.documentElement.appendChild(n);
|
|
2633
|
+
__measureHost = n;
|
|
2634
|
+
return n;
|
|
2635
|
+
}
|
|
2636
|
+
function readTotalTransformMatrix(t) {
|
|
2637
|
+
const host = getMeasureHost();
|
|
2638
|
+
const tmp = document.createElement("div");
|
|
2639
|
+
tmp.style.transformOrigin = "0 0";
|
|
2640
|
+
if (t.baseTransform) tmp.style.transform = t.baseTransform;
|
|
2641
|
+
if (t.rotate) tmp.style.rotate = t.rotate;
|
|
2642
|
+
if (t.scale) tmp.style.scale = t.scale;
|
|
2643
|
+
if (t.translate) tmp.style.translate = t.translate;
|
|
2644
|
+
host.appendChild(tmp);
|
|
2645
|
+
const M = matrixFromComputed(tmp);
|
|
2646
|
+
host.removeChild(tmp);
|
|
2647
|
+
return M;
|
|
2648
|
+
}
|
|
2253
2649
|
|
|
2254
2650
|
// src/core/context.js
|
|
2255
2651
|
function normalizeCachePolicy(v) {
|
|
2256
|
-
if (v === true) return "soft";
|
|
2257
|
-
if (v === false) return "disabled";
|
|
2258
2652
|
if (typeof v === "string") {
|
|
2259
2653
|
const s = v.toLowerCase().trim();
|
|
2260
2654
|
if (s === "disabled" || s === "full" || s === "auto" || s === "soft") return (
|
|
@@ -2282,6 +2676,7 @@ function createContext(options = {}) {
|
|
|
2282
2676
|
iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
|
|
2283
2677
|
localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
|
|
2284
2678
|
excludeFonts: options.excludeFonts ?? void 0,
|
|
2679
|
+
defaultImageUrl: options.defaultImageUrl ?? void 0,
|
|
2285
2680
|
/** Cache policy (disabled|full|auto|soft). Default: "soft" */
|
|
2286
2681
|
/** @type {CachePolicy} */
|
|
2287
2682
|
cache: cachePolicy,
|
|
@@ -2315,7 +2710,7 @@ async function toImg(url, options) {
|
|
|
2315
2710
|
|
|
2316
2711
|
// src/exporters/toCanvas.js
|
|
2317
2712
|
async function toCanvas(url, options) {
|
|
2318
|
-
const { scale
|
|
2713
|
+
const { scale, dpr } = options;
|
|
2319
2714
|
const safari = isSafari();
|
|
2320
2715
|
const img = new Image();
|
|
2321
2716
|
img.crossOrigin = "anonymous";
|
|
@@ -2334,8 +2729,8 @@ async function toCanvas(url, options) {
|
|
|
2334
2729
|
if (safari) {
|
|
2335
2730
|
await new Promise((r) => setTimeout(r, 100));
|
|
2336
2731
|
}
|
|
2337
|
-
const width = img.naturalWidth *
|
|
2338
|
-
const height = img.naturalHeight *
|
|
2732
|
+
const width = img.naturalWidth * scale;
|
|
2733
|
+
const height = img.naturalHeight * scale;
|
|
2339
2734
|
const canvas = document.createElement("canvas");
|
|
2340
2735
|
canvas.width = Math.ceil(width * dpr);
|
|
2341
2736
|
canvas.height = Math.ceil(height * dpr);
|
|
@@ -2409,15 +2804,27 @@ async function snapdom(element, userOptions) {
|
|
|
2409
2804
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
2410
2805
|
const context = createContext(userOptions);
|
|
2411
2806
|
if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
|
|
2807
|
+
if (!context.snap) {
|
|
2808
|
+
context.snap = {
|
|
2809
|
+
toPng: (el, opts) => snapdom.toPng(el, opts),
|
|
2810
|
+
toImg: (el, opts) => snapdom.toImg(el, opts)
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2412
2813
|
return snapdom.capture(element, context, INTERNAL_TOKEN);
|
|
2413
2814
|
}
|
|
2414
2815
|
snapdom.capture = async (el, context, _token) => {
|
|
2415
|
-
if (_token !== INTERNAL_TOKEN)
|
|
2416
|
-
throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
|
|
2417
|
-
}
|
|
2816
|
+
if (_token !== INTERNAL_TOKEN) throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
|
|
2418
2817
|
const url = await captureDOM(el, context);
|
|
2419
2818
|
const ensureContext = (opts) => ({ ...context, ...opts || {} });
|
|
2420
|
-
const withFormat = (format) => (opts) =>
|
|
2819
|
+
const withFormat = (format) => (opts) => {
|
|
2820
|
+
const next = ensureContext({ ...opts || {}, format });
|
|
2821
|
+
const wantsJpeg = format === "jpeg" || format === "jpg";
|
|
2822
|
+
const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
|
|
2823
|
+
if (wantsJpeg && noBg) {
|
|
2824
|
+
next.backgroundColor = "#ffffff";
|
|
2825
|
+
}
|
|
2826
|
+
return rasterize(url, next);
|
|
2827
|
+
};
|
|
2421
2828
|
return {
|
|
2422
2829
|
url,
|
|
2423
2830
|
toRaw: () => url,
|