@zumer/snapdom 1.9.6 → 1.9.8
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 +57 -9
- package/dist/snapdom.js +240 -139
- package/dist/snapdom.min.js +3 -3
- package/dist/snapdom.min.mjs +3 -3
- package/dist/snapdom.mjs +230 -137
- package/package.json +1 -1
- package/types/snapdom.d.ts +1 -0
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.
|
|
4
|
+
* v.1.9.8
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -81,7 +81,7 @@ function getDefaultStyleForTag(tagName) {
|
|
|
81
81
|
if (cache.defaultStyle.has(tagName)) {
|
|
82
82
|
return cache.defaultStyle.get(tagName);
|
|
83
83
|
}
|
|
84
|
-
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
|
|
84
|
+
const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
|
|
85
85
|
if (skipTags.has(tagName)) {
|
|
86
86
|
const empty = {};
|
|
87
87
|
cache.defaultStyle.set(tagName, empty);
|
|
@@ -111,10 +111,14 @@ function getDefaultStyleForTag(tagName) {
|
|
|
111
111
|
cache.defaultStyle.set(tagName, defaults);
|
|
112
112
|
return defaults;
|
|
113
113
|
}
|
|
114
|
+
var IGNORED_PROPS = /* @__PURE__ */ new Set([
|
|
115
|
+
"-webkit-locale"
|
|
116
|
+
]);
|
|
114
117
|
function getStyleKey(snapshot, tagName, compress = false) {
|
|
115
118
|
const entries = [];
|
|
116
119
|
const defaultStyles = getDefaultStyleForTag(tagName);
|
|
117
120
|
for (let [prop, value] of Object.entries(snapshot)) {
|
|
121
|
+
if (IGNORED_PROPS.has(prop)) continue;
|
|
118
122
|
if (!compress) {
|
|
119
123
|
if (value) {
|
|
120
124
|
entries.push(`${prop}:${value}`);
|
|
@@ -164,6 +168,7 @@ function generateCSSClasses() {
|
|
|
164
168
|
const classMap = /* @__PURE__ */ new Map();
|
|
165
169
|
let counter = 1;
|
|
166
170
|
for (const key of keySet) {
|
|
171
|
+
if (!key.trim()) continue;
|
|
167
172
|
classMap.set(key, `c${counter++}`);
|
|
168
173
|
}
|
|
169
174
|
return classMap;
|
|
@@ -395,6 +400,7 @@ function splitBackgroundImage(bg) {
|
|
|
395
400
|
// src/modules/styles.js
|
|
396
401
|
function snapshotComputedStyleFull(style) {
|
|
397
402
|
const result = {};
|
|
403
|
+
const computedVisibility = style.getPropertyValue("visibility");
|
|
398
404
|
for (let i = 0; i < style.length; i++) {
|
|
399
405
|
const prop = style[i];
|
|
400
406
|
let val = style.getPropertyValue(prop);
|
|
@@ -403,6 +409,9 @@ function snapshotComputedStyleFull(style) {
|
|
|
403
409
|
}
|
|
404
410
|
result[prop] = val;
|
|
405
411
|
}
|
|
412
|
+
if (computedVisibility === "hidden") {
|
|
413
|
+
result.opacity = "0";
|
|
414
|
+
}
|
|
406
415
|
return result;
|
|
407
416
|
}
|
|
408
417
|
function inlineAllStyles(source, clone, compress) {
|
|
@@ -519,7 +528,6 @@ function deepClone(node, compress, options = {}, originalRoot) {
|
|
|
519
528
|
if (node.checked) clone.setAttribute("checked", "");
|
|
520
529
|
if (node.indeterminate) clone.indeterminate = node.indeterminate;
|
|
521
530
|
}
|
|
522
|
-
return clone;
|
|
523
531
|
}
|
|
524
532
|
if (node instanceof HTMLSelectElement) {
|
|
525
533
|
pendingSelectValue = node.value;
|
|
@@ -623,25 +631,38 @@ function isIconFont(input) {
|
|
|
623
631
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
624
632
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
625
633
|
const dpr = window.devicePixelRatio || 1;
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
+
await document.fonts.ready;
|
|
635
|
+
const span = document.createElement("span");
|
|
636
|
+
span.textContent = unicodeChar;
|
|
637
|
+
span.style.position = "absolute";
|
|
638
|
+
span.style.visibility = "hidden";
|
|
639
|
+
span.style.fontFamily = `"${fontFamily}"`;
|
|
640
|
+
span.style.fontWeight = fontWeight || "normal";
|
|
641
|
+
span.style.fontSize = `${fontSize}px`;
|
|
642
|
+
span.style.lineHeight = "1";
|
|
643
|
+
span.style.whiteSpace = "nowrap";
|
|
644
|
+
span.style.padding = "0";
|
|
645
|
+
span.style.margin = "0";
|
|
646
|
+
document.body.appendChild(span);
|
|
647
|
+
const rect = span.getBoundingClientRect();
|
|
648
|
+
const width = Math.ceil(rect.width);
|
|
649
|
+
const height = Math.ceil(rect.height);
|
|
650
|
+
document.body.removeChild(span);
|
|
634
651
|
const canvas = document.createElement("canvas");
|
|
635
|
-
canvas.width =
|
|
636
|
-
canvas.height =
|
|
652
|
+
canvas.width = width * dpr;
|
|
653
|
+
canvas.height = height * dpr;
|
|
637
654
|
const ctx = canvas.getContext("2d");
|
|
638
655
|
ctx.scale(dpr, dpr);
|
|
639
|
-
ctx.font =
|
|
656
|
+
ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
|
|
640
657
|
ctx.textAlign = "left";
|
|
641
|
-
ctx.textBaseline = "
|
|
658
|
+
ctx.textBaseline = "top";
|
|
642
659
|
ctx.fillStyle = color;
|
|
643
|
-
ctx.fillText(unicodeChar, 0,
|
|
644
|
-
return
|
|
660
|
+
ctx.fillText(unicodeChar, 0, 0);
|
|
661
|
+
return {
|
|
662
|
+
dataUrl: canvas.toDataURL(),
|
|
663
|
+
width,
|
|
664
|
+
height
|
|
665
|
+
};
|
|
645
666
|
}
|
|
646
667
|
function isStylesheetLoaded(href) {
|
|
647
668
|
return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
|
|
@@ -740,8 +761,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
740
761
|
if (!src || isIconFont(family)) continue;
|
|
741
762
|
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
|
|
742
763
|
const localRegex = /local\((["']?)[^)]+?\1\)/g;
|
|
743
|
-
const hasURL =
|
|
744
|
-
const hasLocal =
|
|
764
|
+
const hasURL = !!src.match(urlRegex);
|
|
765
|
+
const hasLocal = !!src.match(localRegex);
|
|
745
766
|
if (!hasURL && hasLocal) {
|
|
746
767
|
finalCSS += `@font-face{font-family:${family};src:${src};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
|
|
747
768
|
continue;
|
|
@@ -826,18 +847,20 @@ async function embedCustomFonts({ preCached = false } = {}) {
|
|
|
826
847
|
}
|
|
827
848
|
|
|
828
849
|
// src/modules/pseudo.js
|
|
829
|
-
async function inlinePseudoElements(source, clone,
|
|
850
|
+
async function inlinePseudoElements(source, clone, options) {
|
|
830
851
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
831
852
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
832
853
|
try {
|
|
833
854
|
const style = getStyle(source, pseudo);
|
|
834
855
|
if (!style || typeof style[Symbol.iterator] !== "function") continue;
|
|
856
|
+
const isEmptyPseudo = style.content === "none" && style.backgroundImage === "none" && style.backgroundColor === "transparent" && (style.borderStyle === "none" || parseFloat(style.borderWidth) === 0) && (!style.transform || style.transform === "none") && style.display === "inline";
|
|
857
|
+
if (isEmptyPseudo) continue;
|
|
835
858
|
if (pseudo === "::first-letter") {
|
|
836
859
|
const normal = getComputedStyle(source);
|
|
837
860
|
const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
|
|
838
861
|
if (!isMeaningful) continue;
|
|
839
862
|
const textNode = Array.from(clone.childNodes).find(
|
|
840
|
-
(n) => n.nodeType === Node.TEXT_NODE && n.textContent
|
|
863
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim().length > 0
|
|
841
864
|
);
|
|
842
865
|
if (!textNode) continue;
|
|
843
866
|
const text = textNode.textContent;
|
|
@@ -848,74 +871,83 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
|
|
|
848
871
|
const span = document.createElement("span");
|
|
849
872
|
span.textContent = first;
|
|
850
873
|
span.dataset.snapdomPseudo = "::first-letter";
|
|
851
|
-
const
|
|
852
|
-
const
|
|
853
|
-
cache.preStyleMap.set(span,
|
|
874
|
+
const snapshot2 = snapshotComputedStyle(style);
|
|
875
|
+
const key2 = getStyleKey(snapshot2, "span", options);
|
|
876
|
+
cache.preStyleMap.set(span, key2);
|
|
854
877
|
const restNode = document.createTextNode(rest);
|
|
855
878
|
clone.replaceChild(restNode, textNode);
|
|
856
879
|
clone.insertBefore(span, restNode);
|
|
857
880
|
continue;
|
|
858
881
|
}
|
|
859
|
-
const content = style.
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
882
|
+
const content = style.content;
|
|
883
|
+
const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
|
|
884
|
+
const bg = style.backgroundImage;
|
|
885
|
+
const bgColor = style.backgroundColor;
|
|
886
|
+
const fontFamily = style.fontFamily;
|
|
887
|
+
const fontSize = parseInt(style.fontSize) || 32;
|
|
888
|
+
const fontWeight = parseInt(style.fontWeight) || false;
|
|
889
|
+
const color = style.color || "#000";
|
|
890
|
+
const display = style.display;
|
|
891
|
+
const width = parseFloat(style.width);
|
|
892
|
+
const height = parseFloat(style.height);
|
|
893
|
+
const borderStyle = style.borderStyle;
|
|
894
|
+
const borderWidth = parseFloat(style.borderWidth);
|
|
895
|
+
const transform = style.transform;
|
|
896
|
+
const isIconFont2 = isIconFont(fontFamily);
|
|
897
|
+
const hasExplicitContent = content !== "none" && cleanContent !== "";
|
|
863
898
|
const hasBg = bg && bg !== "none";
|
|
864
899
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
pseudoEl.appendChild(imgEl);
|
|
887
|
-
} else if (cleanContent.startsWith("url(")) {
|
|
888
|
-
const rawUrl = extractURL(cleanContent);
|
|
889
|
-
if (rawUrl && rawUrl.trim() !== "") {
|
|
890
|
-
try {
|
|
891
|
-
const imgEl = document.createElement("img");
|
|
892
|
-
const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
|
|
893
|
-
imgEl.src = dataUrl;
|
|
894
|
-
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
895
|
-
pseudoEl.appendChild(imgEl);
|
|
896
|
-
} catch (e) {
|
|
897
|
-
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
} else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
|
|
901
|
-
pseudoEl.textContent = cleanContent;
|
|
902
|
-
}
|
|
903
|
-
if (hasBg) {
|
|
900
|
+
const hasBox = display !== "inline" && (width > 0 || height > 0);
|
|
901
|
+
const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
|
|
902
|
+
const hasTransform = transform && transform !== "none";
|
|
903
|
+
const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
|
|
904
|
+
if (!shouldRender) continue;
|
|
905
|
+
const pseudoEl = document.createElement("span");
|
|
906
|
+
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
907
|
+
pseudoEl.style.verticalAlign = "middle";
|
|
908
|
+
const snapshot = snapshotComputedStyle(style);
|
|
909
|
+
const key = getStyleKey(snapshot, "span", options);
|
|
910
|
+
cache.preStyleMap.set(pseudoEl, key);
|
|
911
|
+
if (isIconFont2 && cleanContent.length === 1) {
|
|
912
|
+
const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
|
|
913
|
+
const imgEl = document.createElement("img");
|
|
914
|
+
imgEl.src = dataUrl;
|
|
915
|
+
imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
|
|
916
|
+
pseudoEl.appendChild(imgEl);
|
|
917
|
+
clone.dataset.snapdomHasIcon = "true";
|
|
918
|
+
} else if (cleanContent.startsWith("url(")) {
|
|
919
|
+
const rawUrl = extractURL(cleanContent);
|
|
920
|
+
if (rawUrl?.trim()) {
|
|
904
921
|
try {
|
|
905
|
-
const
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
pseudoEl.
|
|
922
|
+
const imgEl = document.createElement("img");
|
|
923
|
+
const dataUrl = await fetchImage(safeEncodeURI(rawUrl), options);
|
|
924
|
+
imgEl.src = dataUrl;
|
|
925
|
+
imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
|
|
926
|
+
pseudoEl.appendChild(imgEl);
|
|
910
927
|
} catch (e) {
|
|
911
|
-
console.
|
|
928
|
+
console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
|
|
912
929
|
}
|
|
913
930
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
931
|
+
} else if (!isIconFont2 && hasExplicitContent) {
|
|
932
|
+
pseudoEl.textContent = cleanContent;
|
|
933
|
+
}
|
|
934
|
+
if (hasBg) {
|
|
935
|
+
try {
|
|
936
|
+
const bgSplits = splitBackgroundImage(bg);
|
|
937
|
+
const newBgParts = await Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
|
|
938
|
+
pseudoEl.style.backgroundImage = newBgParts.join(", ");
|
|
939
|
+
} catch (e) {
|
|
940
|
+
console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
944
|
+
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
|
|
945
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
|
|
946
|
+
if (!hasVisibleBox) continue;
|
|
947
|
+
if (pseudo === "::before") {
|
|
948
|
+
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
949
|
+
} else {
|
|
950
|
+
clone.appendChild(pseudoEl);
|
|
919
951
|
}
|
|
920
952
|
} catch (e) {
|
|
921
953
|
console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
|
|
@@ -924,43 +956,57 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
|
|
|
924
956
|
const sChildren = Array.from(source.children);
|
|
925
957
|
const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
|
|
926
958
|
for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
|
|
927
|
-
await inlinePseudoElements(
|
|
928
|
-
sChildren[i],
|
|
929
|
-
cChildren[i],
|
|
930
|
-
compress,
|
|
931
|
-
embedFonts,
|
|
932
|
-
useProxy
|
|
933
|
-
);
|
|
959
|
+
await inlinePseudoElements(sChildren[i], cChildren[i], options);
|
|
934
960
|
}
|
|
935
961
|
}
|
|
936
962
|
|
|
937
963
|
// src/modules/svgDefs.js
|
|
938
|
-
function
|
|
939
|
-
if (!
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
964
|
+
function inlineExternalDefsAndSymbols(rootElement) {
|
|
965
|
+
if (!rootElement) return;
|
|
966
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
967
|
+
rootElement.querySelectorAll("use").forEach((use) => {
|
|
968
|
+
const href = use.getAttribute("xlink:href") || use.getAttribute("href");
|
|
969
|
+
if (href && href.startsWith("#")) {
|
|
970
|
+
usedIds.add(href.slice(1));
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
if (!usedIds.size) return;
|
|
974
|
+
const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
|
|
975
|
+
const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
|
|
976
|
+
const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
|
|
977
|
+
let container = rootElement.querySelector("svg.inline-defs-container");
|
|
978
|
+
if (!container) {
|
|
979
|
+
container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
980
|
+
container.setAttribute("aria-hidden", "true");
|
|
981
|
+
container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
|
|
982
|
+
container.classList.add("inline-defs-container");
|
|
983
|
+
rootElement.insertBefore(container, rootElement.firstChild);
|
|
984
|
+
}
|
|
985
|
+
const existingIds = /* @__PURE__ */ new Set();
|
|
986
|
+
rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
|
|
987
|
+
existingIds.add(el.id);
|
|
988
|
+
});
|
|
989
|
+
usedIds.forEach((id) => {
|
|
990
|
+
if (existingIds.has(id)) return;
|
|
991
|
+
const symbol = globalSymbols.find((sym) => sym.id === id);
|
|
992
|
+
if (symbol) {
|
|
993
|
+
container.appendChild(symbol.cloneNode(true));
|
|
994
|
+
existingIds.add(id);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
for (const defs of globalDefs) {
|
|
998
|
+
const defEl = defs.querySelector(`#${CSS.escape(id)}`);
|
|
999
|
+
if (defEl) {
|
|
1000
|
+
let defsContainer = container.querySelector("defs");
|
|
1001
|
+
if (!defsContainer) {
|
|
1002
|
+
defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
1003
|
+
container.appendChild(defsContainer);
|
|
958
1004
|
}
|
|
1005
|
+
defsContainer.appendChild(defEl.cloneNode(true));
|
|
1006
|
+
existingIds.add(id);
|
|
1007
|
+
break;
|
|
959
1008
|
}
|
|
960
1009
|
}
|
|
961
|
-
if (defs.childNodes.length) {
|
|
962
|
-
svg.insertBefore(defs, svg.firstChild);
|
|
963
|
-
}
|
|
964
1010
|
});
|
|
965
1011
|
}
|
|
966
1012
|
|
|
@@ -968,6 +1014,12 @@ function inlineExternalDef(root) {
|
|
|
968
1014
|
async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
|
|
969
1015
|
let clone;
|
|
970
1016
|
let classCSS = "";
|
|
1017
|
+
stabilizeLayout(element);
|
|
1018
|
+
try {
|
|
1019
|
+
inlineExternalDefsAndSymbols(element);
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1022
|
+
}
|
|
971
1023
|
try {
|
|
972
1024
|
clone = deepClone(element, compress, options, element);
|
|
973
1025
|
} catch (e) {
|
|
@@ -979,21 +1031,24 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
979
1031
|
} catch (e) {
|
|
980
1032
|
console.warn("inlinePseudoElements failed:", e);
|
|
981
1033
|
}
|
|
982
|
-
try {
|
|
983
|
-
inlineExternalDef(clone);
|
|
984
|
-
} catch (e) {
|
|
985
|
-
console.warn("inlineExternalDef failed:", e);
|
|
986
|
-
}
|
|
987
1034
|
if (compress) {
|
|
988
1035
|
const keyToClass = generateCSSClasses();
|
|
989
1036
|
classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
|
|
990
1037
|
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
991
1038
|
if (node.tagName === "STYLE") continue;
|
|
1039
|
+
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
1040
|
+
node.setAttribute("style", key.replace(/;/g, "; "));
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
992
1043
|
const className = keyToClass.get(key);
|
|
993
1044
|
if (className) node.classList.add(className);
|
|
994
1045
|
const bgImage = node.style?.backgroundImage;
|
|
995
|
-
node.
|
|
1046
|
+
const hasIcon = node.dataset?.snapdomHasIcon;
|
|
996
1047
|
if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
|
|
1048
|
+
if (hasIcon) {
|
|
1049
|
+
node.style.verticalAlign = "middle";
|
|
1050
|
+
node.style.display = "inline";
|
|
1051
|
+
}
|
|
997
1052
|
}
|
|
998
1053
|
} else {
|
|
999
1054
|
for (const [node, key] of cache.preStyleMap.entries()) {
|
|
@@ -1043,6 +1098,18 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
|
|
|
1043
1098
|
}
|
|
1044
1099
|
return { clone, classCSS };
|
|
1045
1100
|
}
|
|
1101
|
+
function stabilizeLayout(element) {
|
|
1102
|
+
const style = getComputedStyle(element);
|
|
1103
|
+
const outlineStyle = style.outlineStyle;
|
|
1104
|
+
const outlineWidth = style.outlineWidth;
|
|
1105
|
+
const borderStyle = style.borderStyle;
|
|
1106
|
+
const borderWidth = style.borderWidth;
|
|
1107
|
+
const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
|
|
1108
|
+
const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
|
|
1109
|
+
if (outlineVisible && borderAbsent) {
|
|
1110
|
+
element.style.border = `${outlineWidth} solid transparent`;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1046
1113
|
|
|
1047
1114
|
// src/modules/images.js
|
|
1048
1115
|
async function inlineImages(clone, options = {}) {
|
|
@@ -1078,13 +1145,27 @@ async function inlineBackgroundImages(source, clone, options = {}) {
|
|
|
1078
1145
|
"mask-source",
|
|
1079
1146
|
"mask-box-image-source",
|
|
1080
1147
|
"mask-border-source",
|
|
1081
|
-
"-webkit-mask-box-image-source"
|
|
1148
|
+
"-webkit-mask-box-image-source",
|
|
1149
|
+
"border-image",
|
|
1150
|
+
"border-image-source",
|
|
1151
|
+
"border-image-slice",
|
|
1152
|
+
"border-image-width",
|
|
1153
|
+
"border-image-outset",
|
|
1154
|
+
"border-image-repeat"
|
|
1082
1155
|
];
|
|
1083
1156
|
while (queue.length) {
|
|
1084
1157
|
const [srcNode, cloneNode] = queue.shift();
|
|
1085
1158
|
const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
|
|
1086
1159
|
if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
|
|
1160
|
+
const hasBorderImage = (() => {
|
|
1161
|
+
const bi = style.getPropertyValue("border-image");
|
|
1162
|
+
const bis = style.getPropertyValue("border-image-source");
|
|
1163
|
+
return bi && bi !== "none" || bis && bis !== "none";
|
|
1164
|
+
})();
|
|
1087
1165
|
for (const prop of imageProps) {
|
|
1166
|
+
if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1088
1169
|
const val = style.getPropertyValue(prop);
|
|
1089
1170
|
if (!val || val === "none") continue;
|
|
1090
1171
|
const splits = splitBackgroundImage(val);
|
|
@@ -1186,8 +1267,6 @@ async function captureDOM(element, options = {}) {
|
|
|
1186
1267
|
const existingTransform = clone.style.transform || "";
|
|
1187
1268
|
const scaleTransform = `scale(${scaleX}, ${scaleY})`;
|
|
1188
1269
|
clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
|
|
1189
|
-
} else if (hasScale && isSafari()) {
|
|
1190
|
-
clone.style.scale = `${scale}`;
|
|
1191
1270
|
}
|
|
1192
1271
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
1193
1272
|
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
@@ -1212,33 +1291,47 @@ async function captureDOM(element, options = {}) {
|
|
|
1212
1291
|
}
|
|
1213
1292
|
|
|
1214
1293
|
// src/api/snapdom.js
|
|
1215
|
-
async function toImg(url, {
|
|
1294
|
+
async function toImg(url, { scale = 1 } = {}) {
|
|
1216
1295
|
const img = new Image();
|
|
1217
1296
|
img.src = url;
|
|
1218
1297
|
await img.decode();
|
|
1219
|
-
if (
|
|
1220
|
-
img.width = img.
|
|
1221
|
-
img.height = img.
|
|
1222
|
-
} else {
|
|
1223
|
-
img.width = img.width / scale;
|
|
1224
|
-
img.height = img.height / scale;
|
|
1298
|
+
if (scale !== 1) {
|
|
1299
|
+
img.style.width = `${img.naturalWidth * scale}px`;
|
|
1300
|
+
img.style.height = `${img.naturalHeight * scale}px`;
|
|
1225
1301
|
}
|
|
1226
1302
|
return img;
|
|
1227
1303
|
}
|
|
1228
1304
|
async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
|
|
1229
1305
|
const img = new Image();
|
|
1230
1306
|
img.src = url;
|
|
1307
|
+
img.crossOrigin = "anonymous";
|
|
1308
|
+
img.loading = "eager";
|
|
1309
|
+
img.decoding = "sync";
|
|
1310
|
+
const isSafariBrowser = isSafari();
|
|
1311
|
+
let appended = false;
|
|
1312
|
+
if (isSafariBrowser) {
|
|
1313
|
+
document.body.appendChild(img);
|
|
1314
|
+
appended = true;
|
|
1315
|
+
}
|
|
1231
1316
|
await img.decode();
|
|
1317
|
+
if (isSafariBrowser) {
|
|
1318
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1319
|
+
}
|
|
1320
|
+
if (img.width === 0 || img.height === 0) {
|
|
1321
|
+
if (appended) img.remove();
|
|
1322
|
+
throw new Error("Image failed to load or has no dimensions");
|
|
1323
|
+
}
|
|
1324
|
+
const width = img.naturalWidth * scale;
|
|
1325
|
+
const height = img.naturalHeight * scale;
|
|
1232
1326
|
const canvas = document.createElement("canvas");
|
|
1233
|
-
const width = img.width * scale;
|
|
1234
|
-
const height = img.height * scale;
|
|
1235
1327
|
canvas.width = Math.ceil(width * dpr);
|
|
1236
1328
|
canvas.height = Math.ceil(height * dpr);
|
|
1329
|
+
canvas.style.width = `${width}px`;
|
|
1330
|
+
canvas.style.height = `${height}px`;
|
|
1237
1331
|
const ctx = canvas.getContext("2d");
|
|
1238
1332
|
ctx.scale(dpr, dpr);
|
|
1239
1333
|
ctx.drawImage(img, 0, 0, width, height);
|
|
1240
|
-
|
|
1241
|
-
canvas.style.height = `${height}px`;
|
|
1334
|
+
if (appended) img.remove();
|
|
1242
1335
|
return canvas;
|
|
1243
1336
|
}
|
|
1244
1337
|
async function toBlob(url, {
|
|
@@ -1285,7 +1378,7 @@ async function toRasterImg(url, { dpr = 1, scale = 1, backgroundColor, quality }
|
|
|
1285
1378
|
img.style.height = `${canvas.height / dpr}px`;
|
|
1286
1379
|
return img;
|
|
1287
1380
|
}
|
|
1288
|
-
async function download(url, { dpr = 1, scale = 1, backgroundColor
|
|
1381
|
+
async function download(url, { dpr = 1, scale = 1, backgroundColor, format = "png", filename = "snapDOM" } = {}) {
|
|
1289
1382
|
if (format === "svg") {
|
|
1290
1383
|
const blob = await toBlob(url);
|
|
1291
1384
|
const objectURL = URL.createObjectURL(blob);
|
|
@@ -1321,19 +1414,19 @@ async function snapdom(element, options = {}) {
|
|
|
1321
1414
|
}
|
|
1322
1415
|
snapdom.capture = async (el, options = {}) => {
|
|
1323
1416
|
const url = await captureDOM(el, options);
|
|
1324
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1417
|
+
const dpr = options.dpr ?? (window.devicePixelRatio || 1);
|
|
1325
1418
|
const scale = options.scale || 1;
|
|
1326
1419
|
return {
|
|
1327
1420
|
url,
|
|
1328
1421
|
options,
|
|
1329
1422
|
toRaw: () => url,
|
|
1330
|
-
toImg: () => toImg(url, { dpr, scale }),
|
|
1331
|
-
toCanvas: () => toCanvas(url, { dpr, scale }),
|
|
1332
|
-
toBlob: (
|
|
1333
|
-
toPng: (
|
|
1334
|
-
toJpg: (
|
|
1335
|
-
toWebp: (
|
|
1336
|
-
download: ({ format = "png", filename = "
|
|
1423
|
+
toImg: (opts = {}) => toImg(url, { dpr, scale, ...opts }),
|
|
1424
|
+
toCanvas: (opts = {}) => toCanvas(url, { dpr, scale, ...opts }),
|
|
1425
|
+
toBlob: (opts = {}) => toBlob(url, { dpr, scale, ...opts }),
|
|
1426
|
+
toPng: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "png"),
|
|
1427
|
+
toJpg: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "jpeg"),
|
|
1428
|
+
toWebp: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "webp"),
|
|
1429
|
+
download: ({ format = "png", filename = "snapDOM", backgroundColor, ...opts } = {}) => download(url, { dpr, scale, format, filename, backgroundColor, ...opts })
|
|
1337
1430
|
};
|
|
1338
1431
|
};
|
|
1339
1432
|
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
|
package/package.json
CHANGED