@zumer/snapdom 1.9.12-dev.0 → 1.9.12-dev.2
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 +45 -41
- package/dist/snapdom.js +373 -61
- package/dist/snapdom.min.js +5 -5
- package/dist/snapdom.min.mjs +5 -5
- package/dist/snapdom.mjs +360 -53
- package/package.json +1 -1
- package/types/snapdom.d.ts +5 -5
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.12-dev.
|
|
4
|
+
* v.1.9.12-dev.2
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -626,7 +626,10 @@ function idle(fn, { fast = false } = {}) {
|
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
function isSafari() {
|
|
629
|
-
|
|
629
|
+
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
|
|
630
|
+
const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
|
|
631
|
+
const isWeChatUA = /(MicroMessenger|wxwork|WeCom|WindowsWechat|MacWechat)/i.test(ua);
|
|
632
|
+
return isSafariUA || isWeChatUA;
|
|
630
633
|
}
|
|
631
634
|
|
|
632
635
|
// src/modules/styles.js
|
|
@@ -660,7 +663,7 @@ function setupInvalidationOnce(root = document.documentElement) {
|
|
|
660
663
|
} catch {
|
|
661
664
|
}
|
|
662
665
|
}
|
|
663
|
-
function snapshotComputedStyleFull(style) {
|
|
666
|
+
function snapshotComputedStyleFull(style, options = {}) {
|
|
664
667
|
const out = {};
|
|
665
668
|
const vis = style.getPropertyValue("visibility");
|
|
666
669
|
for (let i = 0; i < style.length; i++) {
|
|
@@ -671,6 +674,24 @@ function snapshotComputedStyleFull(style) {
|
|
|
671
674
|
}
|
|
672
675
|
out[prop] = val;
|
|
673
676
|
}
|
|
677
|
+
if (options.embedFonts) {
|
|
678
|
+
const EXTRA_FONT_PROPS = [
|
|
679
|
+
"font-feature-settings",
|
|
680
|
+
"font-variation-settings",
|
|
681
|
+
"font-kerning",
|
|
682
|
+
"font-variant",
|
|
683
|
+
"font-variant-ligatures",
|
|
684
|
+
"font-optical-sizing"
|
|
685
|
+
];
|
|
686
|
+
for (const prop of EXTRA_FONT_PROPS) {
|
|
687
|
+
if (out[prop]) continue;
|
|
688
|
+
try {
|
|
689
|
+
const v = style.getPropertyValue(prop);
|
|
690
|
+
if (v) out[prop] = v;
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
674
695
|
if (vis === "hidden") out.opacity = "0";
|
|
675
696
|
return out;
|
|
676
697
|
}
|
|
@@ -683,11 +704,12 @@ function styleSignature(snap) {
|
|
|
683
704
|
__snapshotSig.set(snap, sig);
|
|
684
705
|
return sig;
|
|
685
706
|
}
|
|
686
|
-
function getSnapshot(el, preStyle = null) {
|
|
707
|
+
function getSnapshot(el, preStyle = null, options = {}) {
|
|
687
708
|
const rec = snapshotCache.get(el);
|
|
688
709
|
if (rec && rec.epoch === __epoch) return rec.snapshot;
|
|
689
710
|
const style = preStyle || getComputedStyle(el);
|
|
690
|
-
const snap = snapshotComputedStyleFull(style);
|
|
711
|
+
const snap = snapshotComputedStyleFull(style, options);
|
|
712
|
+
stripHeightForWrappers(el, style, snap);
|
|
691
713
|
snapshotCache.set(el, { epoch: __epoch, snapshot: snap });
|
|
692
714
|
return snap;
|
|
693
715
|
}
|
|
@@ -741,7 +763,7 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
741
763
|
session.styleCache.set(source, getComputedStyle(source));
|
|
742
764
|
}
|
|
743
765
|
const pre = session.styleCache.get(source);
|
|
744
|
-
const snap = getSnapshot(source, pre);
|
|
766
|
+
const snap = getSnapshot(source, pre, ctx.options);
|
|
745
767
|
const sig = styleSignature(snap);
|
|
746
768
|
let key = persist.snapshotKeyCache.get(sig);
|
|
747
769
|
if (!key) {
|
|
@@ -751,6 +773,49 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
751
773
|
}
|
|
752
774
|
session.styleMap.set(clone, key);
|
|
753
775
|
}
|
|
776
|
+
function isReplaced(el) {
|
|
777
|
+
return el instanceof HTMLImageElement || el instanceof HTMLCanvasElement || el instanceof HTMLVideoElement || el instanceof HTMLIFrameElement || el instanceof SVGElement || el instanceof HTMLObjectElement || el instanceof HTMLEmbedElement;
|
|
778
|
+
}
|
|
779
|
+
function hasBox(cs) {
|
|
780
|
+
if (cs.backgroundImage && cs.backgroundImage !== "none") return true;
|
|
781
|
+
if (cs.backgroundColor && cs.backgroundColor !== "rgba(0, 0, 0, 0)" && cs.backgroundColor !== "transparent") return true;
|
|
782
|
+
if ((parseFloat(cs.borderTopWidth) || 0) > 0) return true;
|
|
783
|
+
if ((parseFloat(cs.borderBottomWidth) || 0) > 0) return true;
|
|
784
|
+
if ((parseFloat(cs.paddingTop) || 0) > 0) return true;
|
|
785
|
+
if ((parseFloat(cs.paddingBottom) || 0) > 0) return true;
|
|
786
|
+
const ob = cs.overflowBlock || cs.overflowY || "visible";
|
|
787
|
+
return ob !== "visible";
|
|
788
|
+
}
|
|
789
|
+
function isFlexOrGridItem(el) {
|
|
790
|
+
const p = el.parentElement;
|
|
791
|
+
if (!p) return false;
|
|
792
|
+
const pd = getComputedStyle(p).display || "";
|
|
793
|
+
return pd.includes("flex") || pd.includes("grid");
|
|
794
|
+
}
|
|
795
|
+
function hasFlowFast(el, cs) {
|
|
796
|
+
if (el.textContent && /\S/.test(el.textContent)) return true;
|
|
797
|
+
const f = el.firstElementChild, l = el.lastElementChild;
|
|
798
|
+
if (f && f.tagName === "BR" || l && l.tagName === "BR") return true;
|
|
799
|
+
const sh = el.scrollHeight;
|
|
800
|
+
if (sh === 0) return false;
|
|
801
|
+
const pt = parseFloat(cs.paddingTop) || 0;
|
|
802
|
+
const pb = parseFloat(cs.paddingBottom) || 0;
|
|
803
|
+
return sh > pt + pb;
|
|
804
|
+
}
|
|
805
|
+
function stripHeightForWrappers(el, cs, snap) {
|
|
806
|
+
if (el instanceof HTMLElement && el.style && el.style.height) return;
|
|
807
|
+
const disp = cs.display || "";
|
|
808
|
+
if (disp.includes("flex") || disp.includes("grid")) return;
|
|
809
|
+
if (isReplaced(el)) return;
|
|
810
|
+
const pos = cs.position;
|
|
811
|
+
if (pos === "absolute" || pos === "fixed" || pos === "sticky") return;
|
|
812
|
+
if (cs.transform !== "none") return;
|
|
813
|
+
if (hasBox(cs)) return;
|
|
814
|
+
if (isFlexOrGridItem(el)) return;
|
|
815
|
+
if (!hasFlowFast(el, cs)) return;
|
|
816
|
+
delete snap.height;
|
|
817
|
+
delete snap["block-size"];
|
|
818
|
+
}
|
|
754
819
|
|
|
755
820
|
// src/core/clone.js
|
|
756
821
|
function idleCallback(childList, callback, fast) {
|
|
@@ -985,7 +1050,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
985
1050
|
return node.cloneNode(true);
|
|
986
1051
|
}
|
|
987
1052
|
if (node.getAttribute("data-capture") === "exclude") {
|
|
988
|
-
if (options.excludeMode === "
|
|
1053
|
+
if (options.excludeMode === "hide") {
|
|
989
1054
|
const spacer = document.createElement("div");
|
|
990
1055
|
const rect = node.getBoundingClientRect();
|
|
991
1056
|
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
|
|
@@ -998,7 +1063,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
998
1063
|
for (const selector of options.exclude) {
|
|
999
1064
|
try {
|
|
1000
1065
|
if (node.matches?.(selector)) {
|
|
1001
|
-
if (options.excludeMode === "
|
|
1066
|
+
if (options.excludeMode === "hide") {
|
|
1002
1067
|
const spacer = document.createElement("div");
|
|
1003
1068
|
const rect = node.getBoundingClientRect();
|
|
1004
1069
|
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
|
|
@@ -1015,7 +1080,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1015
1080
|
if (typeof options.filter === "function") {
|
|
1016
1081
|
try {
|
|
1017
1082
|
if (!options.filter(node)) {
|
|
1018
|
-
if (options.filterMode === "
|
|
1083
|
+
if (options.filterMode === "hide") {
|
|
1019
1084
|
const spacer = document.createElement("div");
|
|
1020
1085
|
const rect = node.getBoundingClientRect();
|
|
1021
1086
|
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
|
|
@@ -1898,7 +1963,182 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
|
|
|
1898
1963
|
}
|
|
1899
1964
|
}
|
|
1900
1965
|
|
|
1966
|
+
// src/modules/counter.js
|
|
1967
|
+
function hasCounters(input) {
|
|
1968
|
+
return /\bcounter\s*\(|\bcounters\s*\(/.test(input || "");
|
|
1969
|
+
}
|
|
1970
|
+
function unquoteDoubleStrings(s) {
|
|
1971
|
+
return (s || "").replace(/"([^"]*)"/g, "$1");
|
|
1972
|
+
}
|
|
1973
|
+
function alpha(n, upper = false) {
|
|
1974
|
+
let s = "", x = Math.max(1, n);
|
|
1975
|
+
while (x > 0) {
|
|
1976
|
+
x--;
|
|
1977
|
+
s = String.fromCharCode(97 + x % 26) + s;
|
|
1978
|
+
x = Math.floor(x / 26);
|
|
1979
|
+
}
|
|
1980
|
+
return upper ? s.toUpperCase() : s;
|
|
1981
|
+
}
|
|
1982
|
+
function roman(n, upper = true) {
|
|
1983
|
+
const map = [[1e3, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]];
|
|
1984
|
+
let num = Math.max(1, Math.min(3999, n)), out = "";
|
|
1985
|
+
for (const [v, sym] of map) while (num >= v) {
|
|
1986
|
+
out += sym;
|
|
1987
|
+
num -= v;
|
|
1988
|
+
}
|
|
1989
|
+
return upper ? out : out.toLowerCase();
|
|
1990
|
+
}
|
|
1991
|
+
function formatCounter(value, style) {
|
|
1992
|
+
switch ((style || "decimal").toLowerCase()) {
|
|
1993
|
+
case "decimal":
|
|
1994
|
+
return String(Math.max(0, value));
|
|
1995
|
+
case "decimal-leading-zero":
|
|
1996
|
+
return (value < 10 ? "0" : "") + String(Math.max(0, value));
|
|
1997
|
+
case "lower-alpha":
|
|
1998
|
+
return alpha(value, false);
|
|
1999
|
+
case "upper-alpha":
|
|
2000
|
+
return alpha(value, true);
|
|
2001
|
+
case "lower-roman":
|
|
2002
|
+
return roman(value, false);
|
|
2003
|
+
case "upper-roman":
|
|
2004
|
+
return roman(value, true);
|
|
2005
|
+
default:
|
|
2006
|
+
return String(Math.max(0, value));
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function buildCounterContext(root) {
|
|
2010
|
+
const nodeCounters = /* @__PURE__ */ new WeakMap();
|
|
2011
|
+
const rootEl = root instanceof Document ? root.documentElement : root;
|
|
2012
|
+
const isLi = (el) => el && el.tagName === "LI";
|
|
2013
|
+
const countPrevLi = (li) => {
|
|
2014
|
+
let c = 0, p = li?.parentElement;
|
|
2015
|
+
if (!p) return 0;
|
|
2016
|
+
for (const sib of p.children) {
|
|
2017
|
+
if (sib === li) break;
|
|
2018
|
+
if (sib.tagName === "LI") c++;
|
|
2019
|
+
}
|
|
2020
|
+
return c;
|
|
2021
|
+
};
|
|
2022
|
+
const cloneMap = (m) => {
|
|
2023
|
+
const out = /* @__PURE__ */ new Map();
|
|
2024
|
+
for (const [k, arr] of m) out.set(k, arr.slice());
|
|
2025
|
+
return out;
|
|
2026
|
+
};
|
|
2027
|
+
const applyTo = (baseMap, parentMap, el) => {
|
|
2028
|
+
const map = cloneMap(baseMap);
|
|
2029
|
+
let reset;
|
|
2030
|
+
try {
|
|
2031
|
+
reset = el.style?.counterReset || getComputedStyle(el).counterReset;
|
|
2032
|
+
} catch {
|
|
2033
|
+
}
|
|
2034
|
+
if (reset && reset !== "none") {
|
|
2035
|
+
for (const part of reset.split(",")) {
|
|
2036
|
+
const toks = part.trim().split(/\s+/);
|
|
2037
|
+
const name = toks[0];
|
|
2038
|
+
const val = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 0;
|
|
2039
|
+
if (!name) continue;
|
|
2040
|
+
const parentStack = parentMap.get(name);
|
|
2041
|
+
if (parentStack && parentStack.length) {
|
|
2042
|
+
const s = parentStack.slice();
|
|
2043
|
+
s.push(val);
|
|
2044
|
+
map.set(name, s);
|
|
2045
|
+
} else {
|
|
2046
|
+
map.set(name, [val]);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
let inc;
|
|
2051
|
+
try {
|
|
2052
|
+
inc = el.style?.counterIncrement || getComputedStyle(el).counterIncrement;
|
|
2053
|
+
} catch {
|
|
2054
|
+
}
|
|
2055
|
+
if (inc && inc !== "none") {
|
|
2056
|
+
for (const part of inc.split(",")) {
|
|
2057
|
+
const toks = part.trim().split(/\s+/);
|
|
2058
|
+
const name = toks[0];
|
|
2059
|
+
const by = Number.isFinite(Number(toks[1])) ? Number(toks[1]) : 1;
|
|
2060
|
+
if (!name) continue;
|
|
2061
|
+
const stack = map.get(name) || [];
|
|
2062
|
+
if (stack.length === 0) stack.push(0);
|
|
2063
|
+
stack[stack.length - 1] += by;
|
|
2064
|
+
map.set(name, stack);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
try {
|
|
2068
|
+
const cs = getComputedStyle(el);
|
|
2069
|
+
if (cs.display === "list-item" && isLi(el)) {
|
|
2070
|
+
const p = el.parentElement;
|
|
2071
|
+
let idx = 1;
|
|
2072
|
+
if (p && p.tagName === "OL") {
|
|
2073
|
+
const startAttr = p.getAttribute("start");
|
|
2074
|
+
const start = Number.isFinite(Number(startAttr)) ? Number(startAttr) : 1;
|
|
2075
|
+
const prev = countPrevLi(el);
|
|
2076
|
+
const ownAttr = el.getAttribute("value");
|
|
2077
|
+
idx = Number.isFinite(Number(ownAttr)) ? Number(ownAttr) : start + prev;
|
|
2078
|
+
} else {
|
|
2079
|
+
idx = 1 + countPrevLi(el);
|
|
2080
|
+
}
|
|
2081
|
+
const s = map.get("list-item") || [];
|
|
2082
|
+
if (s.length === 0) s.push(0);
|
|
2083
|
+
s[s.length - 1] = idx;
|
|
2084
|
+
map.set("list-item", s);
|
|
2085
|
+
}
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
return map;
|
|
2089
|
+
};
|
|
2090
|
+
const build = (el, parentMap, carryMap) => {
|
|
2091
|
+
const curr = applyTo(carryMap, parentMap, el);
|
|
2092
|
+
nodeCounters.set(el, curr);
|
|
2093
|
+
let nextCarry = curr;
|
|
2094
|
+
for (const child of el.children) {
|
|
2095
|
+
const childCarry = build(child, curr, nextCarry);
|
|
2096
|
+
nextCarry = childCarry;
|
|
2097
|
+
}
|
|
2098
|
+
return curr;
|
|
2099
|
+
};
|
|
2100
|
+
const empty = /* @__PURE__ */ new Map();
|
|
2101
|
+
build(rootEl, empty, empty);
|
|
2102
|
+
return {
|
|
2103
|
+
get(node, name) {
|
|
2104
|
+
const s = nodeCounters.get(node)?.get(name);
|
|
2105
|
+
return s && s.length ? s[s.length - 1] : 0;
|
|
2106
|
+
},
|
|
2107
|
+
getStack(node, name) {
|
|
2108
|
+
const s = nodeCounters.get(node)?.get(name);
|
|
2109
|
+
return s ? s.slice() : [];
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
function resolveCountersInContent(raw, node, ctx) {
|
|
2114
|
+
if (!raw || raw === "none") return raw;
|
|
2115
|
+
try {
|
|
2116
|
+
const RX = /\b(counter|counters)\s*\(([^)]+)\)/g;
|
|
2117
|
+
let out = raw.replace(RX, (_, fn, args) => {
|
|
2118
|
+
const parts = String(args).split(",").map((s) => s.trim());
|
|
2119
|
+
if (fn === "counter") {
|
|
2120
|
+
const name = parts[0]?.replace(/^["']|["']$/g, "");
|
|
2121
|
+
const style = (parts[1] || "decimal").toLowerCase();
|
|
2122
|
+
const v = ctx.get(node, name);
|
|
2123
|
+
return formatCounter(v, style);
|
|
2124
|
+
} else {
|
|
2125
|
+
const name = parts[0]?.replace(/^["']|["']$/g, "");
|
|
2126
|
+
const sep = parts[1]?.replace(/^["']|["']$/g, "") ?? "";
|
|
2127
|
+
const style = (parts[2] || "decimal").toLowerCase();
|
|
2128
|
+
const stack = ctx.getStack(node, name);
|
|
2129
|
+
if (!stack.length) return "";
|
|
2130
|
+
const pieces = stack.map((v) => formatCounter(v, style));
|
|
2131
|
+
return pieces.join(sep);
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
return unquoteDoubleStrings(out);
|
|
2135
|
+
} catch {
|
|
2136
|
+
return "- ";
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
1901
2140
|
// src/modules/pseudo.js
|
|
2141
|
+
var counterCtx = null;
|
|
1902
2142
|
async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
1903
2143
|
if (!(source instanceof Element) || !(clone instanceof Element)) return;
|
|
1904
2144
|
for (const pseudo of ["::before", "::after", "::first-letter"]) {
|
|
@@ -1932,7 +2172,11 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
1932
2172
|
continue;
|
|
1933
2173
|
}
|
|
1934
2174
|
const content = style.content;
|
|
1935
|
-
|
|
2175
|
+
let cleanContent = parseContent(content);
|
|
2176
|
+
if (hasCounters(content)) {
|
|
2177
|
+
counterCtx = buildCounterContext(source.ownerDocument || document);
|
|
2178
|
+
cleanContent = resolveCountersInContent(cleanContent, source, counterCtx);
|
|
2179
|
+
}
|
|
1936
2180
|
const bg = style.backgroundImage;
|
|
1937
2181
|
const bgColor = style.backgroundColor;
|
|
1938
2182
|
const fontFamily = style.fontFamily;
|
|
@@ -1949,10 +2193,10 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
1949
2193
|
const hasExplicitContent = content !== "none" && cleanContent !== "";
|
|
1950
2194
|
const hasBg = bg && bg !== "none";
|
|
1951
2195
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
1952
|
-
const
|
|
2196
|
+
const hasBox2 = display !== "inline" && (width > 0 || height > 0);
|
|
1953
2197
|
const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
|
|
1954
2198
|
const hasTransform = transform && transform !== "none";
|
|
1955
|
-
const shouldRender = hasExplicitContent || hasBg || hasBgColor ||
|
|
2199
|
+
const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox2 || hasBorder || hasTransform;
|
|
1956
2200
|
if (!shouldRender) continue;
|
|
1957
2201
|
const pseudoEl = document.createElement("span");
|
|
1958
2202
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
@@ -1994,7 +2238,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
1994
2238
|
}
|
|
1995
2239
|
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
1996
2240
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
|
|
1997
|
-
const hasVisibleBox = hasContent2 || hasBg || hasBgColor ||
|
|
2241
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox2 || hasBorder || hasTransform;
|
|
1998
2242
|
if (!hasVisibleBox) continue;
|
|
1999
2243
|
if (pseudo === "::before") {
|
|
2000
2244
|
clone.insertBefore(pseudoEl, clone.firstChild);
|
|
@@ -2317,8 +2561,8 @@ async function inlineImages(clone, options = {}) {
|
|
|
2317
2561
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2318
2562
|
return;
|
|
2319
2563
|
}
|
|
2320
|
-
const {
|
|
2321
|
-
if (
|
|
2564
|
+
const { fallbackURL } = options || {};
|
|
2565
|
+
if (fallbackURL) {
|
|
2322
2566
|
try {
|
|
2323
2567
|
const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
|
|
2324
2568
|
const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
|
|
@@ -2326,14 +2570,14 @@ async function inlineImages(clone, options = {}) {
|
|
|
2326
2570
|
const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
|
|
2327
2571
|
const styleW = parseFloat(img.style?.width || "") || 0;
|
|
2328
2572
|
const styleH = parseFloat(img.style?.height || "") || 0;
|
|
2329
|
-
const
|
|
2330
|
-
const
|
|
2331
|
-
const fallbackUrl = typeof
|
|
2573
|
+
const fbW = dsW || styleW || attrW || img.width || void 0;
|
|
2574
|
+
const fbH = dsH || styleH || attrH || img.height || void 0;
|
|
2575
|
+
const fallbackUrl = typeof fallbackURL === "function" ? await fallbackURL({ width: fbW, height: fbH, src, element: img }) : fallbackURL;
|
|
2332
2576
|
if (fallbackUrl) {
|
|
2333
2577
|
const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
|
|
2334
2578
|
img.src = fallbackData.data;
|
|
2335
|
-
if (!img.width &&
|
|
2336
|
-
if (!img.height &&
|
|
2579
|
+
if (!img.width && fbW) img.width = fbW;
|
|
2580
|
+
if (!img.height && fbH) img.height = fbH;
|
|
2337
2581
|
if (!img.width) img.width = img.naturalWidth || 100;
|
|
2338
2582
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2339
2583
|
return;
|
|
@@ -2452,6 +2696,63 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
|
|
|
2452
2696
|
}
|
|
2453
2697
|
}
|
|
2454
2698
|
|
|
2699
|
+
// src/modules/lineClamp.js
|
|
2700
|
+
function lineClamp(el) {
|
|
2701
|
+
if (!el) return () => {
|
|
2702
|
+
};
|
|
2703
|
+
const lines = getClamp(el);
|
|
2704
|
+
if (lines <= 0) return () => {
|
|
2705
|
+
};
|
|
2706
|
+
if (!isPlainTextContainer(el)) return () => {
|
|
2707
|
+
};
|
|
2708
|
+
const cs = getComputedStyle(el);
|
|
2709
|
+
const targetH = Math.round(usedLineHeightPx(cs) * lines + vpad(cs));
|
|
2710
|
+
const original = el.textContent ?? "";
|
|
2711
|
+
const prevText = original;
|
|
2712
|
+
if (el.scrollHeight <= targetH + 0.5) {
|
|
2713
|
+
return () => {
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
let lo = 0, hi = original.length, best = -1;
|
|
2717
|
+
while (lo <= hi) {
|
|
2718
|
+
const mid = lo + hi >> 1;
|
|
2719
|
+
el.textContent = original.slice(0, mid) + "\u2026";
|
|
2720
|
+
if (el.scrollHeight <= targetH + 0.5) {
|
|
2721
|
+
best = mid;
|
|
2722
|
+
lo = mid + 1;
|
|
2723
|
+
} else {
|
|
2724
|
+
hi = mid - 1;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
el.textContent = (best >= 0 ? original.slice(0, best) : "") + "\u2026";
|
|
2728
|
+
return () => {
|
|
2729
|
+
el.textContent = prevText;
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
function getClamp(el) {
|
|
2733
|
+
const cs = getComputedStyle(el);
|
|
2734
|
+
let v = cs.getPropertyValue("-webkit-line-clamp") || cs.getPropertyValue("line-clamp");
|
|
2735
|
+
v = (v || "").trim();
|
|
2736
|
+
const n = parseInt(v, 10);
|
|
2737
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
2738
|
+
}
|
|
2739
|
+
function usedLineHeightPx(cs) {
|
|
2740
|
+
const lh = (cs.lineHeight || "").trim();
|
|
2741
|
+
const fs = parseFloat(cs.fontSize) || 16;
|
|
2742
|
+
if (!lh || lh === "normal") return Math.round(fs * 1.2);
|
|
2743
|
+
if (lh.endsWith("px")) return parseFloat(lh);
|
|
2744
|
+
if (/^\d+(\.\d+)?$/.test(lh)) return Math.round(parseFloat(lh) * fs);
|
|
2745
|
+
if (lh.endsWith("%")) return Math.round(parseFloat(lh) / 100 * fs);
|
|
2746
|
+
return Math.round(fs * 1.2);
|
|
2747
|
+
}
|
|
2748
|
+
function vpad(cs) {
|
|
2749
|
+
return (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
|
|
2750
|
+
}
|
|
2751
|
+
function isPlainTextContainer(el) {
|
|
2752
|
+
if (el.childElementCount > 0) return false;
|
|
2753
|
+
return Array.from(el.childNodes).some((n) => n.nodeType === Node.TEXT_NODE);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2455
2756
|
// src/core/capture.js
|
|
2456
2757
|
async function captureDOM(element, options) {
|
|
2457
2758
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
@@ -2462,7 +2763,12 @@ async function captureDOM(element, options) {
|
|
|
2462
2763
|
let baseCSS = "";
|
|
2463
2764
|
let dataURL;
|
|
2464
2765
|
let svgString;
|
|
2465
|
-
|
|
2766
|
+
const undoClamp = lineClamp(element);
|
|
2767
|
+
try {
|
|
2768
|
+
({ clone, classCSS, styleCache } = await prepareClone(element, options));
|
|
2769
|
+
} finally {
|
|
2770
|
+
undoClamp();
|
|
2771
|
+
}
|
|
2466
2772
|
await new Promise((resolve) => {
|
|
2467
2773
|
idle(async () => {
|
|
2468
2774
|
await inlineImages(clone, options);
|
|
@@ -2512,28 +2818,28 @@ async function captureDOM(element, options) {
|
|
|
2512
2818
|
}
|
|
2513
2819
|
await new Promise((resolve) => {
|
|
2514
2820
|
idle(() => {
|
|
2515
|
-
const rect =
|
|
2516
|
-
let w = rect.width;
|
|
2517
|
-
let h = rect.height;
|
|
2821
|
+
const rect = element.getBoundingClientRect();
|
|
2822
|
+
let w = Number(rect.width) || 0;
|
|
2823
|
+
let h = Number(rect.height) || 0;
|
|
2518
2824
|
const hasW = Number.isFinite(options.width);
|
|
2519
2825
|
const hasH = Number.isFinite(options.height);
|
|
2520
2826
|
const scaleOpt = options.scale ?? 1;
|
|
2521
2827
|
const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
|
|
2522
2828
|
if (!hasScale) {
|
|
2523
|
-
const aspect =
|
|
2829
|
+
const aspect = h > 0 ? w / h : 1;
|
|
2524
2830
|
if (hasW && hasH) {
|
|
2525
|
-
w = options.width;
|
|
2526
|
-
h = options.height;
|
|
2831
|
+
w = Number(options.width);
|
|
2832
|
+
h = Number(options.height);
|
|
2527
2833
|
} else if (hasW) {
|
|
2528
|
-
w = options.width;
|
|
2529
|
-
h = w / aspect;
|
|
2834
|
+
w = Number(options.width);
|
|
2835
|
+
h = w / (aspect || 1);
|
|
2530
2836
|
} else if (hasH) {
|
|
2531
|
-
h = options.height;
|
|
2532
|
-
w = h * aspect;
|
|
2837
|
+
h = Number(options.height);
|
|
2838
|
+
w = h * (aspect || 1);
|
|
2533
2839
|
}
|
|
2534
2840
|
}
|
|
2535
|
-
w = Math.max(1, Math.ceil(w));
|
|
2536
|
-
h = Math.max(1, Math.ceil(h));
|
|
2841
|
+
w = Math.max(1, Math.ceil(Number.isFinite(w) ? w : 1));
|
|
2842
|
+
h = Math.max(1, Math.ceil(Number.isFinite(h) ? h : 1));
|
|
2537
2843
|
const csEl = getComputedStyle(element);
|
|
2538
2844
|
const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
|
|
2539
2845
|
const ind = readIndividualTransforms(element);
|
|
@@ -2604,7 +2910,9 @@ async function captureDOM(element, options) {
|
|
|
2604
2910
|
fo.appendChild(container);
|
|
2605
2911
|
const serializer = new XMLSerializer();
|
|
2606
2912
|
const foString = serializer.serializeToString(fo);
|
|
2607
|
-
const
|
|
2913
|
+
const finalW = !hasScale && (hasW || hasH) ? w : outW;
|
|
2914
|
+
const finalH = !hasScale && (hasH || hasW) ? h : outH;
|
|
2915
|
+
const svgHeader = `<svg xmlns="${svgNS}" width="${finalW}" height="${finalH}" viewBox="0 0 ${outW} ${outH}">`;
|
|
2608
2916
|
const svgFooter = "</svg>";
|
|
2609
2917
|
svgString = svgHeader + foString + svgFooter;
|
|
2610
2918
|
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
@@ -2615,17 +2923,6 @@ async function captureDOM(element, options) {
|
|
|
2615
2923
|
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
2616
2924
|
return dataURL;
|
|
2617
2925
|
}
|
|
2618
|
-
function getNaturalBorderBoxSize(el) {
|
|
2619
|
-
const cs = getComputedStyle(el);
|
|
2620
|
-
const px = (v) => parseFloat(v) || 0;
|
|
2621
|
-
let w = px(cs.width);
|
|
2622
|
-
let h = px(cs.height);
|
|
2623
|
-
if (cs.boxSizing === "content-box") {
|
|
2624
|
-
w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
|
|
2625
|
-
h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
2626
|
-
}
|
|
2627
|
-
return { width: w, height: h };
|
|
2628
|
-
}
|
|
2629
2926
|
function matrixFromComputed(el) {
|
|
2630
2927
|
const tr = getComputedStyle(el).transform;
|
|
2631
2928
|
if (!tr || tr === "none") return new DOMMatrix();
|
|
@@ -2657,24 +2954,33 @@ function bboxFromMatrix(w, h, M2D) {
|
|
|
2657
2954
|
}
|
|
2658
2955
|
function readIndividualTransforms(el) {
|
|
2659
2956
|
const out = { rotate: "0deg", scale: null, translate: null };
|
|
2660
|
-
const map = el.computedStyleMap
|
|
2957
|
+
const map = typeof el.computedStyleMap === "function" ? el.computedStyleMap() : null;
|
|
2661
2958
|
if (map) {
|
|
2662
|
-
const
|
|
2959
|
+
const safeGet = (prop) => {
|
|
2960
|
+
try {
|
|
2961
|
+
if (typeof map.has === "function" && !map.has(prop)) return null;
|
|
2962
|
+
if (typeof map.get !== "function") return null;
|
|
2963
|
+
return map.get(prop);
|
|
2964
|
+
} catch {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
const rot = safeGet("rotate");
|
|
2663
2969
|
if (rot) {
|
|
2664
2970
|
const ang = rot.angle ?? rot;
|
|
2665
|
-
if (ang && "unit" in ang) {
|
|
2971
|
+
if (ang && typeof ang === "object" && "unit" in ang) {
|
|
2666
2972
|
out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
|
|
2667
2973
|
} else {
|
|
2668
2974
|
out.rotate = String(rot);
|
|
2669
2975
|
}
|
|
2670
2976
|
}
|
|
2671
|
-
const sc =
|
|
2977
|
+
const sc = safeGet("scale");
|
|
2672
2978
|
if (sc && sc.length) {
|
|
2673
2979
|
const sx = sc[0]?.value ?? 1;
|
|
2674
2980
|
const sy = sc[1]?.value ?? sx;
|
|
2675
2981
|
out.scale = `${sx} ${sy}`;
|
|
2676
2982
|
}
|
|
2677
|
-
const tr =
|
|
2983
|
+
const tr = safeGet("translate");
|
|
2678
2984
|
if (tr && tr.length) {
|
|
2679
2985
|
const tx = tr[0]?.value ?? 0;
|
|
2680
2986
|
const ty = tr[1]?.value ?? 0;
|
|
@@ -2759,9 +3065,9 @@ function createContext(options = {}) {
|
|
|
2759
3065
|
scale: options.scale ?? 1,
|
|
2760
3066
|
// DOM filters
|
|
2761
3067
|
exclude: options.exclude ?? [],
|
|
2762
|
-
excludeMode: options.excludeMode ?? "
|
|
3068
|
+
excludeMode: options.excludeMode ?? "hide",
|
|
2763
3069
|
filter: options.filter ?? null,
|
|
2764
|
-
filterMode: options.filterMode ?? "
|
|
3070
|
+
filterMode: options.filterMode ?? "hide",
|
|
2765
3071
|
placeholders: options.placeholders !== false,
|
|
2766
3072
|
// default true
|
|
2767
3073
|
// Fuentes
|
|
@@ -2769,7 +3075,7 @@ function createContext(options = {}) {
|
|
|
2769
3075
|
iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
|
|
2770
3076
|
localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
|
|
2771
3077
|
excludeFonts: options.excludeFonts ?? void 0,
|
|
2772
|
-
|
|
3078
|
+
fallbackURL: options.fallbackURL ?? void 0,
|
|
2773
3079
|
/** Cache policy (disabled|full|auto|soft). Default: "soft" */
|
|
2774
3080
|
/** @type {CachePolicy} */
|
|
2775
3081
|
cache: cachePolicy,
|
|
@@ -2855,6 +3161,7 @@ async function rasterize(url, options) {
|
|
|
2855
3161
|
|
|
2856
3162
|
// src/exporters/download.js
|
|
2857
3163
|
async function download(url, options) {
|
|
3164
|
+
options.dpr = 1;
|
|
2858
3165
|
if (options.format === "svg") {
|
|
2859
3166
|
const blob = await toBlob(url, { ...options, type: "svg" });
|
|
2860
3167
|
const objectURL = URL.createObjectURL(blob);
|
package/package.json
CHANGED
package/types/snapdom.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export type BlobType = "svg" | RasterMime;
|
|
|
13
13
|
export type IconFontMatcher = string | RegExp;
|
|
14
14
|
export type CachePolicy = "disabled" | "full" | "auto" | "soft";
|
|
15
15
|
|
|
16
|
-
export type ExcludeMode = '
|
|
17
|
-
export type FilterMode = '
|
|
16
|
+
export type ExcludeMode = 'hide' | 'remove';
|
|
17
|
+
export type FilterMode = 'hide' | 'remove';
|
|
18
18
|
|
|
19
19
|
export interface LocalFontDescriptor {
|
|
20
20
|
/** CSS font-family name (e.g. "Inter"). */
|
|
@@ -92,7 +92,7 @@ export interface CaptureOptions {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Mode applied to excluded nodes of the cloned tree.
|
|
95
|
-
* Default: "
|
|
95
|
+
* Default: "hide"
|
|
96
96
|
*/
|
|
97
97
|
excludeMode?: ExcludeMode;
|
|
98
98
|
|
|
@@ -104,7 +104,7 @@ export interface CaptureOptions {
|
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Mode applied to filtered nodes of the cloned tree.
|
|
107
|
-
* Default: "
|
|
107
|
+
* Default: "hide"
|
|
108
108
|
*/
|
|
109
109
|
filterMode?: FilterMode;
|
|
110
110
|
|
|
@@ -141,7 +141,7 @@ export interface CaptureOptions {
|
|
|
141
141
|
* - String: use as-is.
|
|
142
142
|
* - Callback: receives measured width/height and original src, returns a URL string.
|
|
143
143
|
*/
|
|
144
|
-
|
|
144
|
+
fallbackURL?:
|
|
145
145
|
| string
|
|
146
146
|
| ((
|
|
147
147
|
args: {
|