@zumer/snapdom 1.9.12-dev.3 → 1.9.13
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 +58 -27
- package/dist/snapdom.js +729 -340
- package/dist/snapdom.min.js +7 -4
- package/dist/snapdom.min.mjs +8 -5
- package/dist/snapdom.mjs +716 -335
- package/package.json +7 -2
- package/types/snapdom.d.ts +19 -1
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.4
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -617,8 +617,9 @@ function idle(fn, { fast = false } = {}) {
|
|
|
617
617
|
function isSafari() {
|
|
618
618
|
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
|
|
619
619
|
const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
|
|
620
|
+
const isUIWebView = /AppleWebKit/i.test(ua) && /Mobile/i.test(ua) && !/Safari/i.test(ua);
|
|
620
621
|
const isWeChatUA = /(MicroMessenger|wxwork|WeCom|WindowsWechat|MacWechat)/i.test(ua);
|
|
621
|
-
return isSafariUA || isWeChatUA;
|
|
622
|
+
return isSafariUA || isUIWebView || isWeChatUA;
|
|
622
623
|
}
|
|
623
624
|
|
|
624
625
|
// src/modules/styles.js
|
|
@@ -806,6 +807,64 @@ function stripHeightForWrappers(el, cs, snap) {
|
|
|
806
807
|
delete snap["block-size"];
|
|
807
808
|
}
|
|
808
809
|
|
|
810
|
+
// src/modules/CSSVar.js
|
|
811
|
+
function resolveCSSVars(sourceEl, cloneEl) {
|
|
812
|
+
if (!(sourceEl instanceof Element) || !(cloneEl instanceof Element)) return;
|
|
813
|
+
const styleAttr = sourceEl.getAttribute?.("style");
|
|
814
|
+
let hasVar = !!(styleAttr && styleAttr.includes("var("));
|
|
815
|
+
if (!hasVar && sourceEl.attributes?.length) {
|
|
816
|
+
const attrs = sourceEl.attributes;
|
|
817
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
818
|
+
const a = attrs[i];
|
|
819
|
+
if (a && typeof a.value === "string" && a.value.includes("var(")) {
|
|
820
|
+
hasVar = true;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (!hasVar) return;
|
|
826
|
+
let cs;
|
|
827
|
+
try {
|
|
828
|
+
cs = getComputedStyle(sourceEl);
|
|
829
|
+
} catch {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const author = sourceEl.style;
|
|
833
|
+
if (author && author.length) {
|
|
834
|
+
for (let i = 0; i < author.length; i++) {
|
|
835
|
+
const prop = author[i];
|
|
836
|
+
const val = author.getPropertyValue(prop);
|
|
837
|
+
if (!val || !val.includes("var(")) continue;
|
|
838
|
+
const resolved = cs.getPropertyValue(prop);
|
|
839
|
+
if (resolved) {
|
|
840
|
+
try {
|
|
841
|
+
cloneEl.style.setProperty(prop, resolved.trim(), author.getPropertyPriority(prop));
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (sourceEl.attributes?.length) {
|
|
848
|
+
const attrs = sourceEl.attributes;
|
|
849
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
850
|
+
const a = attrs[i];
|
|
851
|
+
if (!a || typeof a.value !== "string" || !a.value.includes("var(")) continue;
|
|
852
|
+
const propName = a.name;
|
|
853
|
+
let resolved = "";
|
|
854
|
+
try {
|
|
855
|
+
resolved = cs.getPropertyValue(propName);
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
if (resolved) {
|
|
859
|
+
try {
|
|
860
|
+
cloneEl.style.setProperty(propName, resolved.trim());
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
809
868
|
// src/core/clone.js
|
|
810
869
|
function idleCallback(childList, callback, fast) {
|
|
811
870
|
return Promise.all(childList.map((child) => {
|
|
@@ -827,7 +886,7 @@ function idleCallback(childList, callback, fast) {
|
|
|
827
886
|
function addNotSlottedRightmost(sel) {
|
|
828
887
|
sel = sel.trim();
|
|
829
888
|
if (!sel) return sel;
|
|
830
|
-
if (
|
|
889
|
+
if (/:not\(\s*\[data-sd-slotted\]\s*\)\s*$/.test(sel)) return sel;
|
|
831
890
|
return `${sel}:not([data-sd-slotted])`;
|
|
832
891
|
}
|
|
833
892
|
function wrapWithScope(selectorList, scopeSelector, excludeSlotted = true) {
|
|
@@ -1116,7 +1175,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1116
1175
|
inlineAllStyles(node, clone2, sessionCache, options);
|
|
1117
1176
|
const placeholder = document.createElement("div");
|
|
1118
1177
|
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
|
|
1119
|
-
placeholder.style.cssText =
|
|
1178
|
+
placeholder.style.cssText = "color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;";
|
|
1120
1179
|
clone2.appendChild(placeholder);
|
|
1121
1180
|
return clone2;
|
|
1122
1181
|
}
|
|
@@ -1133,6 +1192,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1133
1192
|
let clone;
|
|
1134
1193
|
try {
|
|
1135
1194
|
clone = node.cloneNode(false);
|
|
1195
|
+
resolveCSSVars(node, clone);
|
|
1136
1196
|
sessionCache.nodeMap.set(clone, node);
|
|
1137
1197
|
if (node.tagName === "IMG") {
|
|
1138
1198
|
freezeImgSrcset(node, clone);
|
|
@@ -1188,7 +1248,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1188
1248
|
} else {
|
|
1189
1249
|
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1190
1250
|
resolve(clonedChild || null);
|
|
1191
|
-
}).catch((
|
|
1251
|
+
}).catch(() => {
|
|
1192
1252
|
resolve(null);
|
|
1193
1253
|
});
|
|
1194
1254
|
}
|
|
@@ -1229,7 +1289,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1229
1289
|
markSlottedSubtree(clonedChild);
|
|
1230
1290
|
}
|
|
1231
1291
|
resolve(clonedChild || null);
|
|
1232
|
-
}).catch((
|
|
1292
|
+
}).catch(() => {
|
|
1233
1293
|
resolve(null);
|
|
1234
1294
|
});
|
|
1235
1295
|
};
|
|
@@ -1244,7 +1304,7 @@ async function deepClone(node, sessionCache, options) {
|
|
|
1244
1304
|
if (clonedAssignedNodes.has(child)) return resolve(null);
|
|
1245
1305
|
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1246
1306
|
resolve(clonedChild || null);
|
|
1247
|
-
}).catch((
|
|
1307
|
+
}).catch(() => {
|
|
1248
1308
|
resolve(null);
|
|
1249
1309
|
});
|
|
1250
1310
|
}
|
|
@@ -1307,7 +1367,10 @@ function isIconFont2(input) {
|
|
|
1307
1367
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
1308
1368
|
fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
|
|
1309
1369
|
const dpr = window.devicePixelRatio || 1;
|
|
1310
|
-
|
|
1370
|
+
try {
|
|
1371
|
+
await document.fonts.ready;
|
|
1372
|
+
} catch {
|
|
1373
|
+
}
|
|
1311
1374
|
const span = document.createElement("span");
|
|
1312
1375
|
span.textContent = unicodeChar;
|
|
1313
1376
|
span.style.position = "absolute";
|
|
@@ -1325,8 +1388,8 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
|
|
|
1325
1388
|
const height = Math.ceil(rect.height);
|
|
1326
1389
|
document.body.removeChild(span);
|
|
1327
1390
|
const canvas = document.createElement("canvas");
|
|
1328
|
-
canvas.width = width * dpr;
|
|
1329
|
-
canvas.height = height * dpr;
|
|
1391
|
+
canvas.width = Math.max(1, width * dpr);
|
|
1392
|
+
canvas.height = Math.max(1, height * dpr);
|
|
1330
1393
|
const ctx = canvas.getContext("2d");
|
|
1331
1394
|
ctx.scale(dpr, dpr);
|
|
1332
1395
|
ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
|
|
@@ -1444,6 +1507,303 @@ function familiesFromRequired(required) {
|
|
|
1444
1507
|
}
|
|
1445
1508
|
return out;
|
|
1446
1509
|
}
|
|
1510
|
+
function rewriteRelativeUrls(cssText, baseHref) {
|
|
1511
|
+
if (!cssText) return cssText;
|
|
1512
|
+
return cssText.replace(
|
|
1513
|
+
/url\(\s*(['"]?)([^)'"]+)\1\s*\)/g,
|
|
1514
|
+
(m, q, u) => {
|
|
1515
|
+
const src = (u || "").trim();
|
|
1516
|
+
if (!src || /^data:|^blob:|^https?:|^file:|^about:/i.test(src)) return m;
|
|
1517
|
+
let abs = src;
|
|
1518
|
+
try {
|
|
1519
|
+
abs = new URL(src, baseHref || location.href).href;
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
return `url("${abs}")`;
|
|
1523
|
+
}
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
var IMPORT_ANY_RE = /@import\s+(?:url\(\s*(['"]?)([^)"']+)\1\s*\)|(['"])([^"']+)\3)([^;]*);/g;
|
|
1527
|
+
var MAX_IMPORT_DEPTH = 4;
|
|
1528
|
+
async function inlineImportsAndRewrite(cssText, ownerHref, useProxy) {
|
|
1529
|
+
if (!cssText) return cssText;
|
|
1530
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1531
|
+
function normalizeUrl(u, base) {
|
|
1532
|
+
try {
|
|
1533
|
+
return new URL(u, base || location.href).href;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return u;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
async function resolveOnce(text, baseHref, depth = 0) {
|
|
1539
|
+
if (depth > MAX_IMPORT_DEPTH) {
|
|
1540
|
+
console.warn(`[snapDOM] @import depth exceeded (${MAX_IMPORT_DEPTH}) at ${baseHref}`);
|
|
1541
|
+
return text;
|
|
1542
|
+
}
|
|
1543
|
+
let out = "";
|
|
1544
|
+
let last = 0;
|
|
1545
|
+
let m;
|
|
1546
|
+
while (m = IMPORT_ANY_RE.exec(text)) {
|
|
1547
|
+
out += text.slice(last, m.index);
|
|
1548
|
+
last = IMPORT_ANY_RE.lastIndex;
|
|
1549
|
+
const rawUrl = (m[2] || m[4] || "").trim();
|
|
1550
|
+
const absUrl = normalizeUrl(rawUrl, baseHref);
|
|
1551
|
+
if (visited.has(absUrl)) {
|
|
1552
|
+
console.warn("[snapDOM] Skipping circular import");
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
visited.add(absUrl);
|
|
1556
|
+
let imported = "";
|
|
1557
|
+
try {
|
|
1558
|
+
const r = await snapFetch(absUrl, { as: "text", useProxy, silent: true });
|
|
1559
|
+
if (r.ok && typeof r.data === "string") imported = r.data;
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
if (imported) {
|
|
1563
|
+
imported = rewriteRelativeUrls(imported, absUrl);
|
|
1564
|
+
imported = await resolveOnce(imported, absUrl, depth + 1);
|
|
1565
|
+
out += `
|
|
1566
|
+
/* inlined: ${absUrl} */
|
|
1567
|
+
${imported}
|
|
1568
|
+
`;
|
|
1569
|
+
} else {
|
|
1570
|
+
out += m[0];
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
out += text.slice(last);
|
|
1574
|
+
return out;
|
|
1575
|
+
}
|
|
1576
|
+
let rewritten = rewriteRelativeUrls(cssText, ownerHref || location.href);
|
|
1577
|
+
rewritten = await resolveOnce(rewritten, ownerHref || location.href, 0);
|
|
1578
|
+
return rewritten;
|
|
1579
|
+
}
|
|
1580
|
+
var URL_RE = /url\((["']?)([^"')]+)\1\)/g;
|
|
1581
|
+
var FACE_RE = /@font-face[^{}]*\{[^}]*\}/g;
|
|
1582
|
+
function parseUnicodeRange(ur) {
|
|
1583
|
+
if (!ur) return [];
|
|
1584
|
+
const ranges = [];
|
|
1585
|
+
const parts = ur.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1586
|
+
for (const p of parts) {
|
|
1587
|
+
const m = p.match(/^U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f?]+))?$/);
|
|
1588
|
+
if (!m) continue;
|
|
1589
|
+
const a = m[1], b = m[2];
|
|
1590
|
+
const expand = (hex) => {
|
|
1591
|
+
if (!hex.includes("?")) return parseInt(hex, 16);
|
|
1592
|
+
const min = parseInt(hex.replace(/\?/g, "0"), 16);
|
|
1593
|
+
const max = parseInt(hex.replace(/\?/g, "F"), 16);
|
|
1594
|
+
return [min, max];
|
|
1595
|
+
};
|
|
1596
|
+
if (b) {
|
|
1597
|
+
const A = expand(a), B = expand(b);
|
|
1598
|
+
const min = Array.isArray(A) ? A[0] : A;
|
|
1599
|
+
const max = Array.isArray(B) ? B[1] : B;
|
|
1600
|
+
ranges.push([Math.min(min, max), Math.max(min, max)]);
|
|
1601
|
+
} else {
|
|
1602
|
+
const X = expand(a);
|
|
1603
|
+
if (Array.isArray(X)) ranges.push([X[0], X[1]]);
|
|
1604
|
+
else ranges.push([X, X]);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return ranges;
|
|
1608
|
+
}
|
|
1609
|
+
function unicodeIntersects(used, ranges) {
|
|
1610
|
+
if (!ranges.length) return true;
|
|
1611
|
+
if (!used || used.size === 0) return true;
|
|
1612
|
+
for (const cp of used) {
|
|
1613
|
+
for (const [a, b] of ranges) if (cp >= a && cp <= b) return true;
|
|
1614
|
+
}
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
function extractSrcUrls(srcValue, baseHref) {
|
|
1618
|
+
const urls = [];
|
|
1619
|
+
if (!srcValue) return urls;
|
|
1620
|
+
for (const m of srcValue.matchAll(URL_RE)) {
|
|
1621
|
+
let u = (m[2] || "").trim();
|
|
1622
|
+
if (!u || u.startsWith("data:")) continue;
|
|
1623
|
+
if (!/^https?:/i.test(u)) {
|
|
1624
|
+
try {
|
|
1625
|
+
u = new URL(u, baseHref || location.href).href;
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
urls.push(u);
|
|
1630
|
+
}
|
|
1631
|
+
return urls;
|
|
1632
|
+
}
|
|
1633
|
+
async function inlineUrlsInCssBlock(cssBlock, baseHref, useProxy = "") {
|
|
1634
|
+
let out = cssBlock;
|
|
1635
|
+
for (const m of cssBlock.matchAll(URL_RE)) {
|
|
1636
|
+
const raw = extractURL(m[0]);
|
|
1637
|
+
if (!raw) continue;
|
|
1638
|
+
let abs = raw;
|
|
1639
|
+
if (!abs.startsWith("http") && !abs.startsWith("data:")) {
|
|
1640
|
+
try {
|
|
1641
|
+
abs = new URL(abs, baseHref || location.href).href;
|
|
1642
|
+
} catch {
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (isIconFont2(abs)) continue;
|
|
1646
|
+
if (cache.resource?.has(abs)) {
|
|
1647
|
+
cache.font?.add(abs);
|
|
1648
|
+
out = out.replace(m[0], `url(${cache.resource.get(abs)})`);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (cache.font?.has(abs)) continue;
|
|
1652
|
+
try {
|
|
1653
|
+
const r = await snapFetch(abs, { as: "dataURL", useProxy, silent: true });
|
|
1654
|
+
if (r.ok && typeof r.data === "string") {
|
|
1655
|
+
const b64 = r.data;
|
|
1656
|
+
cache.resource?.set(abs, b64);
|
|
1657
|
+
cache.font?.add(abs);
|
|
1658
|
+
out = out.replace(m[0], `url(${b64})`);
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
console.warn("[snapDOM] Failed to fetch font resource:", abs);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
function subsetFromRanges(ranges) {
|
|
1667
|
+
if (!ranges.length) return null;
|
|
1668
|
+
const hit = (a, b) => ranges.some(([x, y]) => !(y < a || x > b));
|
|
1669
|
+
const latin = hit(0, 255) || hit(305, 305);
|
|
1670
|
+
const latinExt = hit(256, 591) || hit(7680, 7935);
|
|
1671
|
+
const greek = hit(880, 1023);
|
|
1672
|
+
const cyr = hit(1024, 1279);
|
|
1673
|
+
const viet = hit(7840, 7929) || hit(258, 259) || hit(416, 417) || hit(431, 432);
|
|
1674
|
+
if (viet) return "vietnamese";
|
|
1675
|
+
if (cyr) return "cyrillic";
|
|
1676
|
+
if (greek) return "greek";
|
|
1677
|
+
if (latinExt) return "latin-ext";
|
|
1678
|
+
if (latin) return "latin";
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1681
|
+
function buildSimpleExcluder(ex = {}) {
|
|
1682
|
+
const famSet = new Set((ex.families || []).map((s) => String(s).toLowerCase()));
|
|
1683
|
+
const domSet = new Set((ex.domains || []).map((s) => String(s).toLowerCase()));
|
|
1684
|
+
const subSet = new Set((ex.subsets || []).map((s) => String(s).toLowerCase()));
|
|
1685
|
+
return (meta, parsedRanges) => {
|
|
1686
|
+
if (famSet.size && famSet.has(meta.family.toLowerCase())) return true;
|
|
1687
|
+
if (domSet.size) {
|
|
1688
|
+
for (const u of meta.srcUrls) {
|
|
1689
|
+
try {
|
|
1690
|
+
if (domSet.has(new URL(u).host.toLowerCase())) return true;
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (subSet.size) {
|
|
1696
|
+
const label = subsetFromRanges(parsedRanges);
|
|
1697
|
+
if (label && subSet.has(label)) return true;
|
|
1698
|
+
}
|
|
1699
|
+
return false;
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function dedupeFontFaces(cssText) {
|
|
1703
|
+
if (!cssText) return cssText;
|
|
1704
|
+
const FACE_RE_G = /@font-face[^{}]*\{[^}]*\}/gi;
|
|
1705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1706
|
+
const out = [];
|
|
1707
|
+
for (const block of cssText.match(FACE_RE_G) || []) {
|
|
1708
|
+
const familyRaw = block.match(/font-family:\s*([^;]+);/i)?.[1] || "";
|
|
1709
|
+
const family = pickPrimaryFamily(familyRaw);
|
|
1710
|
+
const weightSpec = (block.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
|
|
1711
|
+
const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1712
|
+
const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1713
|
+
const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1714
|
+
const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1715
|
+
const urls = extractSrcUrls(srcRaw, location.href);
|
|
1716
|
+
const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
|
|
1717
|
+
const key = [
|
|
1718
|
+
String(family || "").toLowerCase(),
|
|
1719
|
+
weightSpec,
|
|
1720
|
+
styleSpec,
|
|
1721
|
+
stretchSpec,
|
|
1722
|
+
urange.toLowerCase(),
|
|
1723
|
+
srcPart
|
|
1724
|
+
].join("|");
|
|
1725
|
+
if (!seen.has(key)) {
|
|
1726
|
+
seen.add(key);
|
|
1727
|
+
out.push(block);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (out.length === 0) return cssText;
|
|
1731
|
+
let i = 0;
|
|
1732
|
+
return cssText.replace(FACE_RE_G, () => out[i++] || "");
|
|
1733
|
+
}
|
|
1734
|
+
function buildFontsCacheKey(required, exclude, localFonts, useProxy) {
|
|
1735
|
+
const req = Array.from(required || []).sort().join("|");
|
|
1736
|
+
const ex = exclude ? JSON.stringify({
|
|
1737
|
+
families: (exclude.families || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1738
|
+
domains: (exclude.domains || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1739
|
+
subsets: (exclude.subsets || []).map((s) => String(s).toLowerCase()).sort()
|
|
1740
|
+
}) : "";
|
|
1741
|
+
const lf = (localFonts || []).map((f) => `${(f.family || "").toLowerCase()}::${f.weight || "normal"}::${f.style || "normal"}::${f.src || ""}`).sort().join("|");
|
|
1742
|
+
const px = useProxy || "";
|
|
1743
|
+
return `fonts-embed-css::req=${req}::ex=${ex}::lf=${lf}::px=${px}`;
|
|
1744
|
+
}
|
|
1745
|
+
async function collectFacesFromSheet(sheet, baseHref, emitFace, ctx) {
|
|
1746
|
+
let rules;
|
|
1747
|
+
try {
|
|
1748
|
+
rules = sheet.cssRules || [];
|
|
1749
|
+
} catch {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const normalizeUrl = (u, base) => {
|
|
1753
|
+
try {
|
|
1754
|
+
return new URL(u, base || location.href).href;
|
|
1755
|
+
} catch {
|
|
1756
|
+
return u;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
for (const rule of rules) {
|
|
1760
|
+
if (rule.type === CSSRule.IMPORT_RULE && rule.styleSheet) {
|
|
1761
|
+
const childHref = rule.href ? normalizeUrl(rule.href, baseHref) : baseHref;
|
|
1762
|
+
if (ctx.depth >= MAX_IMPORT_DEPTH) {
|
|
1763
|
+
console.warn(`[snapDOM] CSSOM import depth exceeded (${MAX_IMPORT_DEPTH}) at ${childHref}`);
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
if (childHref && ctx.visitedSheets.has(childHref)) {
|
|
1767
|
+
console.warn(`[snapDOM] Skipping circular CSSOM import: ${childHref}`);
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (childHref) ctx.visitedSheets.add(childHref);
|
|
1771
|
+
const nextCtx = { ...ctx, depth: (ctx.depth || 0) + 1 };
|
|
1772
|
+
await collectFacesFromSheet(rule.styleSheet, childHref, emitFace, nextCtx);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
1776
|
+
const famRaw = (rule.style.getPropertyValue("font-family") || "").trim();
|
|
1777
|
+
const family = pickPrimaryFamily(famRaw);
|
|
1778
|
+
if (!family || isIconFont2(family)) continue;
|
|
1779
|
+
const weightSpec = (rule.style.getPropertyValue("font-weight") || "400").trim();
|
|
1780
|
+
const styleSpec = (rule.style.getPropertyValue("font-style") || "normal").trim();
|
|
1781
|
+
const stretchSpec = (rule.style.getPropertyValue("font-stretch") || "100%").trim();
|
|
1782
|
+
const srcRaw = (rule.style.getPropertyValue("src") || "").trim();
|
|
1783
|
+
const urange = (rule.style.getPropertyValue("unicode-range") || "").trim();
|
|
1784
|
+
if (!ctx.faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
|
|
1785
|
+
const ranges = parseUnicodeRange(urange);
|
|
1786
|
+
if (!unicodeIntersects(ctx.usedCodepoints, ranges)) continue;
|
|
1787
|
+
const meta = {
|
|
1788
|
+
family,
|
|
1789
|
+
weightSpec,
|
|
1790
|
+
styleSpec,
|
|
1791
|
+
stretchSpec,
|
|
1792
|
+
unicodeRange: urange,
|
|
1793
|
+
srcRaw,
|
|
1794
|
+
srcUrls: extractSrcUrls(srcRaw, baseHref || location.href),
|
|
1795
|
+
href: baseHref || location.href
|
|
1796
|
+
};
|
|
1797
|
+
if (ctx.simpleExcluder && ctx.simpleExcluder(meta, ranges)) continue;
|
|
1798
|
+
if (/url\(/i.test(srcRaw)) {
|
|
1799
|
+
const inlinedSrc = await inlineUrlsInCssBlock(srcRaw, baseHref || location.href, ctx.useProxy);
|
|
1800
|
+
await emitFace(`@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`);
|
|
1801
|
+
} else {
|
|
1802
|
+
await emitFace(`@font-face{font-family:${family};src:${srcRaw};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1447
1807
|
async function embedCustomFonts({
|
|
1448
1808
|
required,
|
|
1449
1809
|
usedCodepoints,
|
|
@@ -1461,9 +1821,6 @@ async function embedCustomFonts({
|
|
|
1461
1821
|
arr.push({ w: parseInt(w, 10), s, st: parseInt(st, 10) });
|
|
1462
1822
|
requiredIndex.set(fam, arr);
|
|
1463
1823
|
}
|
|
1464
|
-
const IMPORT_RE = /@import\s+url\(["']?([^"')]+)["']?\)/g;
|
|
1465
|
-
const URL_RE = /url\((["']?)([^"')]+)\1\)/g;
|
|
1466
|
-
const FACE_RE = /@font-face[^{}]*\{[^}]*\}/g;
|
|
1467
1824
|
function faceMatchesRequired(fam, styleSpec, weightSpec, stretchSpec) {
|
|
1468
1825
|
if (!requiredIndex.has(fam)) return false;
|
|
1469
1826
|
const need = requiredIndex.get(fam);
|
|
@@ -1489,188 +1846,23 @@ async function embedCustomFonts({
|
|
|
1489
1846
|
const sOk = styleOK(normStyle(r.s));
|
|
1490
1847
|
const tOk = r.st >= ts.min && r.st <= ts.max;
|
|
1491
1848
|
const nearWeight = Math.abs(faceSingleW - r.w) <= 300;
|
|
1492
|
-
if (nearWeight && sOk && tOk)
|
|
1493
|
-
return true;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
return false;
|
|
1498
|
-
}
|
|
1499
|
-
function parseUnicodeRange(ur) {
|
|
1500
|
-
if (!ur) return [];
|
|
1501
|
-
const ranges = [];
|
|
1502
|
-
const parts = ur.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1503
|
-
for (const p of parts) {
|
|
1504
|
-
const m = p.match(/^U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f?]+))?$/);
|
|
1505
|
-
if (!m) continue;
|
|
1506
|
-
const a = m[1], b = m[2];
|
|
1507
|
-
const expand = (hex) => {
|
|
1508
|
-
if (!hex.includes("?")) return parseInt(hex, 16);
|
|
1509
|
-
const min = parseInt(hex.replace(/\?/g, "0"), 16);
|
|
1510
|
-
const max = parseInt(hex.replace(/\?/g, "F"), 16);
|
|
1511
|
-
return [min, max];
|
|
1512
|
-
};
|
|
1513
|
-
if (b) {
|
|
1514
|
-
const A = expand(a), B = expand(b);
|
|
1515
|
-
const min = Array.isArray(A) ? A[0] : A;
|
|
1516
|
-
const max = Array.isArray(B) ? B[1] : B;
|
|
1517
|
-
ranges.push([Math.min(min, max), Math.max(min, max)]);
|
|
1518
|
-
} else {
|
|
1519
|
-
const X = expand(a);
|
|
1520
|
-
if (Array.isArray(X)) ranges.push([X[0], X[1]]);
|
|
1521
|
-
else ranges.push([X, X]);
|
|
1849
|
+
if (nearWeight && sOk && tOk) return true;
|
|
1522
1850
|
}
|
|
1523
1851
|
}
|
|
1524
|
-
return ranges;
|
|
1525
|
-
}
|
|
1526
|
-
function unicodeIntersects(used, ranges) {
|
|
1527
|
-
if (!ranges.length) return true;
|
|
1528
|
-
if (!used || used.size === 0) return true;
|
|
1529
|
-
for (const cp of used) {
|
|
1530
|
-
for (const [a, b] of ranges) if (cp >= a && cp <= b) return true;
|
|
1531
|
-
}
|
|
1532
1852
|
return false;
|
|
1533
1853
|
}
|
|
1534
|
-
function extractSrcUrls(srcValue, baseHref) {
|
|
1535
|
-
const urls = [];
|
|
1536
|
-
if (!srcValue) return urls;
|
|
1537
|
-
for (const m of srcValue.matchAll(URL_RE)) {
|
|
1538
|
-
let u = (m[2] || "").trim();
|
|
1539
|
-
if (!u || u.startsWith("data:")) continue;
|
|
1540
|
-
if (!/^https?:/i.test(u)) {
|
|
1541
|
-
try {
|
|
1542
|
-
u = new URL(u, baseHref || location.href).href;
|
|
1543
|
-
} catch {
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
urls.push(u);
|
|
1547
|
-
}
|
|
1548
|
-
return urls;
|
|
1549
|
-
}
|
|
1550
|
-
async function inlineUrlsInCssBlock(cssBlock, baseHref) {
|
|
1551
|
-
let out = cssBlock;
|
|
1552
|
-
for (const m of cssBlock.matchAll(URL_RE)) {
|
|
1553
|
-
const raw = extractURL(m[0]);
|
|
1554
|
-
if (!raw) continue;
|
|
1555
|
-
let abs = raw;
|
|
1556
|
-
if (!abs.startsWith("http") && !abs.startsWith("data:")) {
|
|
1557
|
-
try {
|
|
1558
|
-
abs = new URL(abs, baseHref || location.href).href;
|
|
1559
|
-
} catch {
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
if (isIconFont2(abs)) continue;
|
|
1563
|
-
if (cache.resource?.has(abs)) {
|
|
1564
|
-
cache.font?.add(abs);
|
|
1565
|
-
out = out.replace(m[0], `url(${cache.resource.get(abs)})`);
|
|
1566
|
-
continue;
|
|
1567
|
-
}
|
|
1568
|
-
if (cache.font?.has(abs)) continue;
|
|
1569
|
-
try {
|
|
1570
|
-
const r = await snapFetch(abs, { as: "dataURL", useProxy, silent: true });
|
|
1571
|
-
if (r.ok && typeof r.data === "string") {
|
|
1572
|
-
const b64 = r.data;
|
|
1573
|
-
cache.resource?.set(abs, b64);
|
|
1574
|
-
cache.font?.add(abs);
|
|
1575
|
-
out = out.replace(m[0], `url(${b64})`);
|
|
1576
|
-
}
|
|
1577
|
-
} catch {
|
|
1578
|
-
console.warn("[snapDOM] Failed to fetch font resource:", abs);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
return out;
|
|
1582
|
-
}
|
|
1583
|
-
function subsetFromRanges(ranges) {
|
|
1584
|
-
if (!ranges.length) return null;
|
|
1585
|
-
const hit = (a, b) => ranges.some(([x, y]) => !(y < a || x > b));
|
|
1586
|
-
const latin = hit(0, 255) || hit(305, 305);
|
|
1587
|
-
const latinExt = hit(256, 591) || hit(7680, 7935);
|
|
1588
|
-
const greek = hit(880, 1023);
|
|
1589
|
-
const cyr = hit(1024, 1279);
|
|
1590
|
-
const viet = hit(7840, 7929) || hit(258, 259) || hit(416, 417) || hit(431, 432);
|
|
1591
|
-
if (viet) return "vietnamese";
|
|
1592
|
-
if (cyr) return "cyrillic";
|
|
1593
|
-
if (greek) return "greek";
|
|
1594
|
-
if (latinExt) return "latin-ext";
|
|
1595
|
-
if (latin) return "latin";
|
|
1596
|
-
return null;
|
|
1597
|
-
}
|
|
1598
|
-
function buildSimpleExcluder(ex = {}) {
|
|
1599
|
-
const famSet = new Set((ex.families || []).map((s) => String(s).toLowerCase()));
|
|
1600
|
-
const domSet = new Set((ex.domains || []).map((s) => String(s).toLowerCase()));
|
|
1601
|
-
const subSet = new Set((ex.subsets || []).map((s) => String(s).toLowerCase()));
|
|
1602
|
-
return (meta, parsedRanges) => {
|
|
1603
|
-
if (famSet.size && famSet.has(meta.family.toLowerCase())) return true;
|
|
1604
|
-
if (domSet.size) {
|
|
1605
|
-
for (const u of meta.srcUrls) {
|
|
1606
|
-
try {
|
|
1607
|
-
if (domSet.has(new URL(u).host.toLowerCase())) return true;
|
|
1608
|
-
} catch {
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
if (subSet.size) {
|
|
1613
|
-
const label = subsetFromRanges(parsedRanges);
|
|
1614
|
-
if (label && subSet.has(label)) return true;
|
|
1615
|
-
}
|
|
1616
|
-
return false;
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
1854
|
const simpleExcluder = buildSimpleExcluder(exclude);
|
|
1620
|
-
|
|
1621
|
-
if (!cssText) return cssText;
|
|
1622
|
-
const FACE_RE2 = /@font-face[^{}]*\{[^}]*\}/gi;
|
|
1623
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1624
|
-
const out = [];
|
|
1625
|
-
for (const block of cssText.match(FACE_RE2) || []) {
|
|
1626
|
-
const familyRaw = block.match(/font-family:\s*([^;]+);/i)?.[1] || "";
|
|
1627
|
-
const family = pickPrimaryFamily(familyRaw);
|
|
1628
|
-
const weightSpec = (block.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
|
|
1629
|
-
const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1630
|
-
const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1631
|
-
const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1632
|
-
const srcRaw = (block.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1633
|
-
const urls = extractSrcUrls(srcRaw, location.href);
|
|
1634
|
-
const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
|
|
1635
|
-
const key = [
|
|
1636
|
-
String(family || "").toLowerCase(),
|
|
1637
|
-
weightSpec,
|
|
1638
|
-
styleSpec,
|
|
1639
|
-
stretchSpec,
|
|
1640
|
-
urange.toLowerCase(),
|
|
1641
|
-
srcPart
|
|
1642
|
-
].join("|");
|
|
1643
|
-
if (!seen.has(key)) {
|
|
1644
|
-
seen.add(key);
|
|
1645
|
-
out.push(block);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
if (out.length === 0) return cssText;
|
|
1649
|
-
let i = 0;
|
|
1650
|
-
return cssText.replace(FACE_RE2, () => out[i++] || "");
|
|
1651
|
-
}
|
|
1652
|
-
function buildFontsCacheKey() {
|
|
1653
|
-
const req = Array.from(required || []).sort().join("|");
|
|
1654
|
-
const ex = exclude ? JSON.stringify({
|
|
1655
|
-
families: (exclude.families || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1656
|
-
domains: (exclude.domains || []).map((s) => String(s).toLowerCase()).sort(),
|
|
1657
|
-
subsets: (exclude.subsets || []).map((s) => String(s).toLowerCase()).sort()
|
|
1658
|
-
}) : "";
|
|
1659
|
-
const lf = (localFonts || []).map((f) => `${(f.family || "").toLowerCase()}::${f.weight || "normal"}::${f.style || "normal"}::${f.src || ""}`).sort().join("|");
|
|
1660
|
-
const px = useProxy || "";
|
|
1661
|
-
return `fonts-embed-css::req=${req}::ex=${ex}::lf=${lf}::px=${px}`;
|
|
1662
|
-
}
|
|
1663
|
-
const cacheKey = buildFontsCacheKey();
|
|
1855
|
+
const cacheKey = buildFontsCacheKey(required, exclude, localFonts, useProxy);
|
|
1664
1856
|
if (cache.resource?.has(cacheKey)) {
|
|
1665
|
-
|
|
1666
|
-
return css;
|
|
1857
|
+
return cache.resource.get(cacheKey);
|
|
1667
1858
|
}
|
|
1668
1859
|
const requiredFamilies = familiesFromRequired(required);
|
|
1669
1860
|
const importUrls = [];
|
|
1861
|
+
const IMPORT_ANY_RE_LOCAL = IMPORT_ANY_RE;
|
|
1670
1862
|
for (const styleTag of document.querySelectorAll("style")) {
|
|
1671
1863
|
const cssText = styleTag.textContent || "";
|
|
1672
|
-
for (const m of cssText.matchAll(
|
|
1673
|
-
const u = m[
|
|
1864
|
+
for (const m of cssText.matchAll(IMPORT_ANY_RE_LOCAL)) {
|
|
1865
|
+
const u = (m[2] || m[4] || "").trim();
|
|
1674
1866
|
if (!u || isIconFont2(u)) continue;
|
|
1675
1867
|
const hasLink = !!document.querySelector(`link[rel="stylesheet"][href="${u}"]`);
|
|
1676
1868
|
if (!hasLink) importUrls.push(u);
|
|
@@ -1717,65 +1909,53 @@ async function embedCustomFonts({
|
|
|
1717
1909
|
cssText = res.data;
|
|
1718
1910
|
if (isIconFont2(link.href)) continue;
|
|
1719
1911
|
}
|
|
1912
|
+
cssText = await inlineImportsAndRewrite(cssText, link.href, useProxy);
|
|
1720
1913
|
let facesOut = "";
|
|
1721
1914
|
for (const face of cssText.match(FACE_RE) || []) {
|
|
1722
1915
|
const famRaw = (face.match(/font-family:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1723
1916
|
const family = pickPrimaryFamily(famRaw);
|
|
1724
|
-
if (!family || isIconFont2(family))
|
|
1725
|
-
continue;
|
|
1726
|
-
}
|
|
1917
|
+
if (!family || isIconFont2(family)) continue;
|
|
1727
1918
|
const weightSpec = (face.match(/font-weight:\s*([^;]+);/i)?.[1] || "400").trim();
|
|
1728
1919
|
const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1729
1920
|
const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1730
1921
|
const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1731
1922
|
const srcRaw = (face.match(/src\s*:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1732
1923
|
const srcUrls = extractSrcUrls(srcRaw, link.href);
|
|
1733
|
-
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec))
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1924
|
+
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
|
|
1736
1925
|
const ranges = parseUnicodeRange(urange);
|
|
1737
|
-
if (!unicodeIntersects(usedCodepoints, ranges))
|
|
1738
|
-
continue;
|
|
1739
|
-
}
|
|
1926
|
+
if (!unicodeIntersects(usedCodepoints, ranges)) continue;
|
|
1740
1927
|
const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: link.href };
|
|
1741
|
-
if (exclude && simpleExcluder(meta, ranges))
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href) : face;
|
|
1745
|
-
facesOut += newFace + "";
|
|
1928
|
+
if (exclude && simpleExcluder(meta, ranges)) continue;
|
|
1929
|
+
const newFace = /url\(/i.test(srcRaw) ? await inlineUrlsInCssBlock(face, link.href, useProxy) : face;
|
|
1930
|
+
facesOut += newFace;
|
|
1746
1931
|
}
|
|
1747
|
-
if (facesOut.trim()) finalCSS += facesOut
|
|
1748
|
-
} catch
|
|
1749
|
-
console.warn("[
|
|
1932
|
+
if (facesOut.trim()) finalCSS += facesOut;
|
|
1933
|
+
} catch {
|
|
1934
|
+
console.warn("[snapDOM] Failed to process stylesheet:", link.href);
|
|
1750
1935
|
}
|
|
1751
1936
|
}
|
|
1937
|
+
const ctx = {
|
|
1938
|
+
requiredIndex,
|
|
1939
|
+
usedCodepoints,
|
|
1940
|
+
faceMatchesRequired,
|
|
1941
|
+
simpleExcluder: exclude ? buildSimpleExcluder(exclude) : null,
|
|
1942
|
+
useProxy,
|
|
1943
|
+
visitedSheets: /* @__PURE__ */ new Set(),
|
|
1944
|
+
depth: 0
|
|
1945
|
+
};
|
|
1752
1946
|
for (const sheet of document.styleSheets) {
|
|
1947
|
+
if (sheet.href && linkNodes.some((l) => l.href === sheet.href)) continue;
|
|
1753
1948
|
try {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
const srcRaw = (rule.style.getPropertyValue("src") || "").trim();
|
|
1765
|
-
const urange = (rule.style.getPropertyValue("unicode-range") || "").trim();
|
|
1766
|
-
const srcUrls = extractSrcUrls(srcRaw, sheet.href || location.href);
|
|
1767
|
-
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
|
|
1768
|
-
const ranges = parseUnicodeRange(urange);
|
|
1769
|
-
if (!unicodeIntersects(usedCodepoints, ranges)) continue;
|
|
1770
|
-
const meta = { family, weightSpec, styleSpec, stretchSpec, unicodeRange: urange, srcRaw, srcUrls, href: sheet.href || location.href };
|
|
1771
|
-
if (exclude && simpleExcluder(meta, ranges)) continue;
|
|
1772
|
-
if (/url\(/i.test(srcRaw)) {
|
|
1773
|
-
const inlinedSrc = await inlineUrlsInCssBlock(srcRaw, sheet.href || location.href);
|
|
1774
|
-
finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
|
|
1775
|
-
} else {
|
|
1776
|
-
finalCSS += `@font-face{font-family:${family};src:${srcRaw};font-style:${styleSpec};font-weight:${weightSpec};font-stretch:${stretchSpec};${urange ? `unicode-range:${urange};` : ""}}`;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1949
|
+
const rootHref = sheet.href || location.href;
|
|
1950
|
+
if (rootHref) ctx.visitedSheets.add(rootHref);
|
|
1951
|
+
await collectFacesFromSheet(
|
|
1952
|
+
sheet,
|
|
1953
|
+
rootHref,
|
|
1954
|
+
async (faceCss) => {
|
|
1955
|
+
finalCSS += faceCss;
|
|
1956
|
+
},
|
|
1957
|
+
ctx
|
|
1958
|
+
);
|
|
1779
1959
|
} catch {
|
|
1780
1960
|
}
|
|
1781
1961
|
}
|
|
@@ -1804,7 +1984,7 @@ async function embedCustomFonts({
|
|
|
1804
1984
|
continue;
|
|
1805
1985
|
}
|
|
1806
1986
|
} catch {
|
|
1807
|
-
console.warn("[
|
|
1987
|
+
console.warn("[snapDOM] Failed to fetch dynamic font src:", f._snapdomSrc);
|
|
1808
1988
|
continue;
|
|
1809
1989
|
}
|
|
1810
1990
|
}
|
|
@@ -1839,7 +2019,7 @@ async function embedCustomFonts({
|
|
|
1839
2019
|
continue;
|
|
1840
2020
|
}
|
|
1841
2021
|
} catch {
|
|
1842
|
-
console.warn("[
|
|
2022
|
+
console.warn("[snapDOM] Failed to fetch localFonts src:", src);
|
|
1843
2023
|
continue;
|
|
1844
2024
|
}
|
|
1845
2025
|
}
|
|
@@ -1928,7 +2108,7 @@ async function ensureFontsReady(families, warmupRepetitions = 2) {
|
|
|
1928
2108
|
if (fams.length === 0) return;
|
|
1929
2109
|
const warmupOnce = () => {
|
|
1930
2110
|
const container = document.createElement("div");
|
|
1931
|
-
container.style.cssText =
|
|
2111
|
+
container.style.cssText = "position:absolute!important;left:-9999px!important;top:0!important;opacity:0!important;pointer-events:none!important;contain:layout size style;";
|
|
1932
2112
|
for (const fam of fams) {
|
|
1933
2113
|
const span = document.createElement("span");
|
|
1934
2114
|
span.textContent = "AaBbGg1234\xC1\xC9\xCD\xD3\xDA\xE7\xF1\u2014\u221E";
|
|
@@ -2279,9 +2459,6 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
2279
2459
|
const fontSize = parseInt(style.fontSize) || 32;
|
|
2280
2460
|
const fontWeight = parseInt(style.fontWeight) || false;
|
|
2281
2461
|
const color = style.color || "#000";
|
|
2282
|
-
const display = style.display;
|
|
2283
|
-
const width = parseFloat(style.width);
|
|
2284
|
-
const height = parseFloat(style.height);
|
|
2285
2462
|
const borderStyle = style.borderStyle;
|
|
2286
2463
|
const borderWidth = parseFloat(style.borderWidth);
|
|
2287
2464
|
const transform = style.transform;
|
|
@@ -2289,14 +2466,13 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
2289
2466
|
const hasExplicitContent = rawContent !== "none" && cleanContent !== "";
|
|
2290
2467
|
const hasBg = bg && bg !== "none";
|
|
2291
2468
|
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
|
|
2292
|
-
const hasBox2 = display !== "inline" && (width > 0 || height > 0);
|
|
2293
2469
|
const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
|
|
2294
2470
|
const hasTransform = transform && transform !== "none";
|
|
2295
|
-
const shouldRender = hasExplicitContent || hasBg || hasBgColor ||
|
|
2471
|
+
const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBorder || hasTransform;
|
|
2296
2472
|
if (!shouldRender) {
|
|
2297
2473
|
if (incs && incs.length && source.parentElement) {
|
|
2298
2474
|
const map = __siblingCounters.get(source.parentElement) || /* @__PURE__ */ new Map();
|
|
2299
|
-
for (const { name
|
|
2475
|
+
for (const { name } of incs) {
|
|
2300
2476
|
if (!name) continue;
|
|
2301
2477
|
const baseWithSibs = withSiblingOverrides(source, counterCtx);
|
|
2302
2478
|
const derived = deriveCounterCtxForPseudo(source, getComputedStyle(source, pseudo), baseWithSibs);
|
|
@@ -2310,6 +2486,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
2310
2486
|
const pseudoEl = document.createElement("span");
|
|
2311
2487
|
pseudoEl.dataset.snapdomPseudo = pseudo;
|
|
2312
2488
|
pseudoEl.style.verticalAlign = "middle";
|
|
2489
|
+
pseudoEl.style.pointerEvents = "none";
|
|
2313
2490
|
const snapshot = snapshotComputedStyle(style);
|
|
2314
2491
|
const key = getStyleKey(snapshot, "span");
|
|
2315
2492
|
sessionCache.styleMap.set(pseudoEl, key);
|
|
@@ -2336,6 +2513,10 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
2336
2513
|
} else if (!isIconFont22 && hasExplicitContent) {
|
|
2337
2514
|
pseudoEl.textContent = cleanContent;
|
|
2338
2515
|
}
|
|
2516
|
+
pseudoEl.style.background = "none";
|
|
2517
|
+
if ("mask" in pseudoEl.style) {
|
|
2518
|
+
pseudoEl.style.mask = "none";
|
|
2519
|
+
}
|
|
2339
2520
|
if (hasBg) {
|
|
2340
2521
|
try {
|
|
2341
2522
|
const bgSplits = splitBackgroundImage(bg);
|
|
@@ -2347,7 +2528,7 @@ async function inlinePseudoElements(source, clone, sessionCache, options) {
|
|
|
2347
2528
|
}
|
|
2348
2529
|
if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
|
|
2349
2530
|
const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
|
|
2350
|
-
const hasVisibleBox = hasContent2 || hasBg || hasBgColor ||
|
|
2531
|
+
const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBorder || hasTransform;
|
|
2351
2532
|
if (incs && incs.length && source.parentElement) {
|
|
2352
2533
|
const map = __siblingCounters.get(source.parentElement) || /* @__PURE__ */ new Map();
|
|
2353
2534
|
const baseWithSibs = withSiblingOverrides(source, counterCtx);
|
|
@@ -2426,6 +2607,102 @@ function inlineExternalDefsAndSymbols(rootElement) {
|
|
|
2426
2607
|
});
|
|
2427
2608
|
}
|
|
2428
2609
|
|
|
2610
|
+
// src/modules/changeCSS.js
|
|
2611
|
+
function freezeSticky(originalRoot, cloneRoot) {
|
|
2612
|
+
if (!originalRoot || !cloneRoot) return;
|
|
2613
|
+
const scrollTop = originalRoot.scrollTop || 0;
|
|
2614
|
+
if (!scrollTop) return;
|
|
2615
|
+
if (getComputedStyle(cloneRoot).position === "static") {
|
|
2616
|
+
cloneRoot.style.position = "relative";
|
|
2617
|
+
}
|
|
2618
|
+
const rootRect = originalRoot.getBoundingClientRect();
|
|
2619
|
+
const viewportH = originalRoot.clientHeight;
|
|
2620
|
+
const PH_ATTR = "data-snap-ph";
|
|
2621
|
+
const walker = document.createTreeWalker(originalRoot, NodeFilter.SHOW_ELEMENT);
|
|
2622
|
+
while (walker.nextNode()) {
|
|
2623
|
+
const el = (
|
|
2624
|
+
/** @type {HTMLElement} */
|
|
2625
|
+
walker.currentNode
|
|
2626
|
+
);
|
|
2627
|
+
const cs = getComputedStyle(el);
|
|
2628
|
+
const pos = cs.position;
|
|
2629
|
+
if (pos !== "sticky" && pos !== "-webkit-sticky") continue;
|
|
2630
|
+
const topInit = _toPx(cs.top);
|
|
2631
|
+
const bottomInit = _toPx(cs.bottom);
|
|
2632
|
+
if (topInit == null && bottomInit == null) continue;
|
|
2633
|
+
const path = _pathOf(el, originalRoot);
|
|
2634
|
+
const cloneEl = _findByPathIgnoringPlaceholders(cloneRoot, path, PH_ATTR);
|
|
2635
|
+
if (!cloneEl) continue;
|
|
2636
|
+
const elRect = el.getBoundingClientRect();
|
|
2637
|
+
const widthPx = elRect.width;
|
|
2638
|
+
const heightPx = elRect.height;
|
|
2639
|
+
const leftPx = elRect.left - rootRect.left;
|
|
2640
|
+
if (!(widthPx > 0 && heightPx > 0)) continue;
|
|
2641
|
+
if (!Number.isFinite(leftPx)) continue;
|
|
2642
|
+
const topAbsPx = topInit != null ? topInit + scrollTop : scrollTop + (viewportH - heightPx - /** bottomInit non-null */
|
|
2643
|
+
bottomInit);
|
|
2644
|
+
if (!Number.isFinite(topAbsPx)) continue;
|
|
2645
|
+
const zParsed = Number.parseInt(cs.zIndex, 10);
|
|
2646
|
+
const hasZ = Number.isFinite(zParsed);
|
|
2647
|
+
const overlayZ = hasZ ? Math.max(zParsed, 1) + 1 : 2;
|
|
2648
|
+
const placeholderZ = hasZ ? zParsed - 1 : 0;
|
|
2649
|
+
const ph = cloneEl.cloneNode(false);
|
|
2650
|
+
ph.setAttribute(PH_ATTR, "1");
|
|
2651
|
+
ph.style.position = "sticky";
|
|
2652
|
+
ph.style.left = `${leftPx}px`;
|
|
2653
|
+
ph.style.top = `${topAbsPx}px`;
|
|
2654
|
+
ph.style.width = `${widthPx}px`;
|
|
2655
|
+
ph.style.height = `${heightPx}px`;
|
|
2656
|
+
ph.style.visibility = "hidden";
|
|
2657
|
+
ph.style.zIndex = String(placeholderZ);
|
|
2658
|
+
ph.style.overflow = "hidden";
|
|
2659
|
+
ph.style.background = "transparent";
|
|
2660
|
+
ph.style.boxShadow = "none";
|
|
2661
|
+
ph.style.filter = "none";
|
|
2662
|
+
cloneEl.parentElement?.insertBefore(ph, cloneEl);
|
|
2663
|
+
cloneEl.style.position = "absolute";
|
|
2664
|
+
cloneEl.style.left = `${leftPx}px`;
|
|
2665
|
+
cloneEl.style.top = `${topAbsPx}px`;
|
|
2666
|
+
cloneEl.style.bottom = "auto";
|
|
2667
|
+
cloneEl.style.zIndex = String(overlayZ);
|
|
2668
|
+
cloneEl.style.pointerEvents = "none";
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function _toPx(v) {
|
|
2672
|
+
if (!v || v === "auto") return null;
|
|
2673
|
+
const n = Number.parseFloat(v);
|
|
2674
|
+
return Number.isFinite(n) ? n : null;
|
|
2675
|
+
}
|
|
2676
|
+
function _pathOf(el, root) {
|
|
2677
|
+
const path = [];
|
|
2678
|
+
for (let cur = el; cur && cur !== root; ) {
|
|
2679
|
+
const p = cur.parentElement;
|
|
2680
|
+
if (!p) break;
|
|
2681
|
+
path.push(Array.prototype.indexOf.call(p.children, cur));
|
|
2682
|
+
cur = p;
|
|
2683
|
+
}
|
|
2684
|
+
return path.reverse();
|
|
2685
|
+
}
|
|
2686
|
+
function _findByPathIgnoringPlaceholders(root, path, phAttr) {
|
|
2687
|
+
let cur = root;
|
|
2688
|
+
for (let i = 0; i < path.length; i++) {
|
|
2689
|
+
const kids = _childrenWithoutPlaceholders(cur, phAttr);
|
|
2690
|
+
cur = /** @type {HTMLElement|undefined} */
|
|
2691
|
+
kids[path[i]];
|
|
2692
|
+
if (!cur) return null;
|
|
2693
|
+
}
|
|
2694
|
+
return cur instanceof HTMLElement ? cur : null;
|
|
2695
|
+
}
|
|
2696
|
+
function _childrenWithoutPlaceholders(el, phAttr) {
|
|
2697
|
+
const out = [];
|
|
2698
|
+
const ch = el.children;
|
|
2699
|
+
for (let i = 0; i < ch.length; i++) {
|
|
2700
|
+
const c = ch[i];
|
|
2701
|
+
if (!c.hasAttribute(phAttr)) out.push(c);
|
|
2702
|
+
}
|
|
2703
|
+
return out;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2429
2706
|
// src/core/prepare.js
|
|
2430
2707
|
async function prepareClone(element, options = {}) {
|
|
2431
2708
|
const sessionCache = {
|
|
@@ -2500,6 +2777,8 @@ async function prepareClone(element, options = {}) {
|
|
|
2500
2777
|
cloneNode.appendChild(inner);
|
|
2501
2778
|
}
|
|
2502
2779
|
}
|
|
2780
|
+
const contentRoot = clone instanceof HTMLElement && clone.firstElementChild instanceof HTMLElement ? clone.firstElementChild : clone;
|
|
2781
|
+
freezeSticky(element, contentRoot);
|
|
2503
2782
|
if (element === sessionCache.nodeMap.get(clone)) {
|
|
2504
2783
|
const computed = sessionCache.styleCache.get(element) || window.getComputedStyle(element);
|
|
2505
2784
|
sessionCache.styleCache.set(element, computed);
|
|
@@ -2875,18 +3154,49 @@ function isPlainTextContainer(el) {
|
|
|
2875
3154
|
}
|
|
2876
3155
|
|
|
2877
3156
|
// src/core/capture.js
|
|
3157
|
+
function stripRootShadows(originalEl, cloneRoot) {
|
|
3158
|
+
if (!originalEl || !cloneRoot || !cloneRoot.style) return;
|
|
3159
|
+
const cs = getComputedStyle(originalEl);
|
|
3160
|
+
try {
|
|
3161
|
+
cloneRoot.style.boxShadow = "none";
|
|
3162
|
+
} catch {
|
|
3163
|
+
}
|
|
3164
|
+
try {
|
|
3165
|
+
cloneRoot.style.textShadow = "none";
|
|
3166
|
+
} catch {
|
|
3167
|
+
}
|
|
3168
|
+
try {
|
|
3169
|
+
cloneRoot.style.outline = "none";
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
const f = cs.filter || "";
|
|
3173
|
+
const cleaned = f.replace(/\bblur\([^()]*\)\s*/gi, "").replace(/\bdrop-shadow\([^()]*\)\s*/gi, "").trim().replace(/\s+/g, " ");
|
|
3174
|
+
try {
|
|
3175
|
+
cloneRoot.style.filter = cleaned.length ? cleaned : "none";
|
|
3176
|
+
} catch {
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
2878
3179
|
async function captureDOM(element, options) {
|
|
2879
3180
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
2880
3181
|
applyCachePolicy(options.cache);
|
|
2881
3182
|
const fast = options.fast;
|
|
3183
|
+
const straighten = !!options.straighten;
|
|
3184
|
+
const noShadows = !!options.noShadows;
|
|
2882
3185
|
let clone, classCSS, styleCache;
|
|
2883
3186
|
let fontsCSS = "";
|
|
2884
3187
|
let baseCSS = "";
|
|
2885
3188
|
let dataURL;
|
|
2886
3189
|
let svgString;
|
|
3190
|
+
let rootTransform2D = null;
|
|
2887
3191
|
const undoClamp = lineClamp(element);
|
|
2888
3192
|
try {
|
|
2889
3193
|
({ clone, classCSS, styleCache } = await prepareClone(element, options));
|
|
3194
|
+
if (straighten && clone) {
|
|
3195
|
+
rootTransform2D = normalizeRootTransforms(element, clone);
|
|
3196
|
+
}
|
|
3197
|
+
if (noShadows && clone) {
|
|
3198
|
+
stripRootShadows(element, clone);
|
|
3199
|
+
}
|
|
2890
3200
|
} finally {
|
|
2891
3201
|
undoClamp();
|
|
2892
3202
|
}
|
|
@@ -2911,7 +3221,7 @@ async function captureDOM(element, options) {
|
|
|
2911
3221
|
const families = new Set(
|
|
2912
3222
|
Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
|
|
2913
3223
|
);
|
|
2914
|
-
await ensureFontsReady(families,
|
|
3224
|
+
await ensureFontsReady(families, 1);
|
|
2915
3225
|
}
|
|
2916
3226
|
fontsCSS = await embedCustomFonts({
|
|
2917
3227
|
required,
|
|
@@ -2959,60 +3269,7 @@ async function captureDOM(element, options) {
|
|
|
2959
3269
|
b = Math.max(b, extY + Math.max(oy, 0));
|
|
2960
3270
|
t = Math.max(t, extY + Math.max(-oy, 0));
|
|
2961
3271
|
}
|
|
2962
|
-
return {
|
|
2963
|
-
bleed: { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b), left: Math.ceil(l) },
|
|
2964
|
-
has: found
|
|
2965
|
-
};
|
|
2966
|
-
}
|
|
2967
|
-
function parseBoxShadow(cs) {
|
|
2968
|
-
const v = cs.boxShadow || "";
|
|
2969
|
-
if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2970
|
-
const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
|
|
2971
|
-
let t = 0, r = 0, b2 = 0, l = 0;
|
|
2972
|
-
for (const part of parts) {
|
|
2973
|
-
const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
|
|
2974
|
-
if (nums.length < 2) continue;
|
|
2975
|
-
const [ox2, oy2, blur = 0, spread = 0] = nums;
|
|
2976
|
-
const extX = Math.abs(ox2) + blur + spread;
|
|
2977
|
-
const extY = Math.abs(oy2) + blur + spread;
|
|
2978
|
-
r = Math.max(r, extX + Math.max(ox2, 0));
|
|
2979
|
-
l = Math.max(l, extX + Math.max(-ox2, 0));
|
|
2980
|
-
b2 = Math.max(b2, extY + Math.max(oy2, 0));
|
|
2981
|
-
t = Math.max(t, extY + Math.max(-oy2, 0));
|
|
2982
|
-
}
|
|
2983
|
-
return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
|
|
2984
|
-
}
|
|
2985
|
-
function parseFilterBlur(cs) {
|
|
2986
|
-
const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
|
|
2987
|
-
const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
|
|
2988
|
-
return { top: b2, right: b2, bottom: b2, left: b2 };
|
|
2989
|
-
}
|
|
2990
|
-
function parseOutline(cs) {
|
|
2991
|
-
if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2992
|
-
const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
|
|
2993
|
-
return { top: w2, right: w2, bottom: w2, left: w2 };
|
|
2994
|
-
}
|
|
2995
|
-
function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
|
|
2996
|
-
const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
|
|
2997
|
-
function pt(x, y) {
|
|
2998
|
-
let X = x - ox2, Y = y - oy2;
|
|
2999
|
-
let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
|
|
3000
|
-
X2 += ox2 + e2;
|
|
3001
|
-
Y2 += oy2 + f2;
|
|
3002
|
-
return [X2, Y2];
|
|
3003
|
-
}
|
|
3004
|
-
const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
|
|
3005
|
-
let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
|
|
3006
|
-
for (const [X, Y] of P) {
|
|
3007
|
-
if (X < minX2) minX2 = X;
|
|
3008
|
-
if (Y < minY2) minY2 = Y;
|
|
3009
|
-
if (X > maxX2) maxX2 = X;
|
|
3010
|
-
if (Y > maxY2) maxY2 = Y;
|
|
3011
|
-
}
|
|
3012
|
-
return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
|
|
3013
|
-
}
|
|
3014
|
-
function hasTFBBox(el) {
|
|
3015
|
-
return hasBBoxAffectingTransform(el);
|
|
3272
|
+
return { bleed: { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b), left: Math.ceil(l) }, has: found };
|
|
3016
3273
|
}
|
|
3017
3274
|
const rect = element.getBoundingClientRect();
|
|
3018
3275
|
const w0 = Math.max(1, Math.ceil(element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
|
|
@@ -3041,28 +3298,38 @@ async function captureDOM(element, options) {
|
|
|
3041
3298
|
h = h0;
|
|
3042
3299
|
}
|
|
3043
3300
|
let minX = 0, minY = 0, maxX = w0, maxY = h0;
|
|
3044
|
-
if (
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3301
|
+
if (straighten && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
|
|
3302
|
+
const M2 = { a: rootTransform2D.a, b: rootTransform2D.b || 0, c: rootTransform2D.c || 0, d: rootTransform2D.d || 1, e: 0, f: 0 };
|
|
3303
|
+
const bb2 = bboxWithOriginFull(w0, h0, M2, 0, 0);
|
|
3304
|
+
minX = bb2.minX;
|
|
3305
|
+
minY = bb2.minY;
|
|
3306
|
+
maxX = bb2.maxX;
|
|
3307
|
+
maxY = bb2.maxY;
|
|
3308
|
+
} else {
|
|
3309
|
+
const useTFBBox = !straighten && hasTFBBox(element);
|
|
3310
|
+
if (useTFBBox) {
|
|
3311
|
+
const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
|
|
3312
|
+
const ind2 = readIndividualTransforms(element);
|
|
3313
|
+
const TOTAL = readTotalTransformMatrix({
|
|
3314
|
+
baseTransform: baseTransform2,
|
|
3315
|
+
rotate: ind2.rotate || "0deg",
|
|
3316
|
+
scale: ind2.scale,
|
|
3317
|
+
translate: ind2.translate
|
|
3318
|
+
});
|
|
3319
|
+
const { ox: ox2, oy: oy2 } = parseTransformOriginPx(csEl, w0, h0);
|
|
3320
|
+
const M = TOTAL.is2D ? TOTAL : new DOMMatrix(TOTAL.toString());
|
|
3321
|
+
const bb = bboxWithOriginFull(w0, h0, M, ox2, oy2);
|
|
3322
|
+
minX = bb.minX;
|
|
3323
|
+
minY = bb.minY;
|
|
3324
|
+
maxX = bb.maxX;
|
|
3325
|
+
maxY = bb.maxY;
|
|
3326
|
+
}
|
|
3060
3327
|
}
|
|
3061
3328
|
const bleedShadow = parseBoxShadow(csEl);
|
|
3062
3329
|
const bleedBlur = parseFilterBlur(csEl);
|
|
3063
3330
|
const bleedOutline = parseOutline(csEl);
|
|
3064
3331
|
const drop = parseFilterDropShadows(csEl);
|
|
3065
|
-
const bleed = {
|
|
3332
|
+
const bleed = noShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
|
|
3066
3333
|
top: bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top,
|
|
3067
3334
|
right: bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right,
|
|
3068
3335
|
bottom: bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom,
|
|
@@ -3077,7 +3344,9 @@ async function captureDOM(element, options) {
|
|
|
3077
3344
|
const outW = Math.max(1, Math.round(vbW0 * (hasW || hasH ? w / w0 : 1)));
|
|
3078
3345
|
const outH = Math.max(1, Math.round(vbH0 * (hasH || hasW ? h / h0 : 1)));
|
|
3079
3346
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
3080
|
-
const
|
|
3347
|
+
const basePad = isSafari() ? 1 : 0;
|
|
3348
|
+
const extraPad = straighten ? 1 : 0;
|
|
3349
|
+
const pad = basePad + extraPad;
|
|
3081
3350
|
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
3082
3351
|
const vbMinX = Math.floor(minX);
|
|
3083
3352
|
const vbMinY = Math.floor(minY);
|
|
@@ -3102,20 +3371,7 @@ async function captureDOM(element, options) {
|
|
|
3102
3371
|
const vbW = vbW0 + pad * 2;
|
|
3103
3372
|
const vbH = vbH0 + pad * 2;
|
|
3104
3373
|
const wantsSize = hasW || hasH;
|
|
3105
|
-
options.meta = {
|
|
3106
|
-
w0,
|
|
3107
|
-
// ancho natural del elemento
|
|
3108
|
-
h0,
|
|
3109
|
-
// alto natural
|
|
3110
|
-
vbW,
|
|
3111
|
-
// ancho del viewBox
|
|
3112
|
-
vbH,
|
|
3113
|
-
// alto del viewBox
|
|
3114
|
-
targetW: w,
|
|
3115
|
-
// ancho deseado según options.width
|
|
3116
|
-
targetH: h
|
|
3117
|
-
// alto deseado según options.height
|
|
3118
|
-
};
|
|
3374
|
+
options.meta = { w0, h0, vbW, vbH, targetW: w, targetH: h };
|
|
3119
3375
|
const svgOutW = isSafari() && wantsSize ? vbW : outW + pad * 2;
|
|
3120
3376
|
const svgOutH = isSafari() && wantsSize ? vbH : outH + pad * 2;
|
|
3121
3377
|
const svgHeader = `<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
|
|
@@ -3129,6 +3385,115 @@ async function captureDOM(element, options) {
|
|
|
3129
3385
|
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
3130
3386
|
return dataURL;
|
|
3131
3387
|
}
|
|
3388
|
+
function normalizeRootTransforms(originalEl, cloneRoot) {
|
|
3389
|
+
if (!originalEl || !cloneRoot || !cloneRoot.style) return null;
|
|
3390
|
+
const cs = getComputedStyle(originalEl);
|
|
3391
|
+
try {
|
|
3392
|
+
cloneRoot.style.transformOrigin = "0 0";
|
|
3393
|
+
} catch {
|
|
3394
|
+
}
|
|
3395
|
+
try {
|
|
3396
|
+
if ("translate" in cloneRoot.style) cloneRoot.style.translate = "none";
|
|
3397
|
+
if ("rotate" in cloneRoot.style) cloneRoot.style.rotate = "none";
|
|
3398
|
+
} catch {
|
|
3399
|
+
}
|
|
3400
|
+
const tr = cs.transform || "none";
|
|
3401
|
+
if (!tr || tr === "none") {
|
|
3402
|
+
try {
|
|
3403
|
+
const M = matrixFromComputed(originalEl);
|
|
3404
|
+
if (M.a === 1 && M.b === 0 && M.c === 0 && M.d === 1) {
|
|
3405
|
+
cloneRoot.style.transform = "none";
|
|
3406
|
+
return { a: 1, b: 0, c: 0, d: 1 };
|
|
3407
|
+
}
|
|
3408
|
+
} catch {
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
const m2d = tr.match(/^matrix\(\s*([^)]+)\)$/i);
|
|
3412
|
+
if (m2d) {
|
|
3413
|
+
const nums = m2d[1].split(",").map((v) => parseFloat(v.trim()));
|
|
3414
|
+
if (nums.length === 6 && nums.every(Number.isFinite)) {
|
|
3415
|
+
const [a, b, c, d] = nums;
|
|
3416
|
+
const scaleX = Math.sqrt(a * a + b * b) || 0;
|
|
3417
|
+
let a1 = 0, b1 = 0, shear = 0, c2 = 0, d2 = 0, scaleY = 0;
|
|
3418
|
+
if (scaleX > 0) {
|
|
3419
|
+
a1 = a / scaleX;
|
|
3420
|
+
b1 = b / scaleX;
|
|
3421
|
+
shear = a1 * c + b1 * d;
|
|
3422
|
+
c2 = c - a1 * shear;
|
|
3423
|
+
d2 = d - b1 * shear;
|
|
3424
|
+
scaleY = Math.sqrt(c2 * c2 + d2 * d2) || 0;
|
|
3425
|
+
if (scaleY > 0) shear = shear / scaleY;
|
|
3426
|
+
else shear = 0;
|
|
3427
|
+
}
|
|
3428
|
+
const aP = scaleX;
|
|
3429
|
+
const bP = 0;
|
|
3430
|
+
const cP = shear * scaleY;
|
|
3431
|
+
const dP = scaleY;
|
|
3432
|
+
try {
|
|
3433
|
+
cloneRoot.style.transform = `matrix(${aP}, ${bP}, ${cP}, ${dP}, 0, 0)`;
|
|
3434
|
+
} catch {
|
|
3435
|
+
}
|
|
3436
|
+
return { a: aP, b: bP, c: cP, d: dP };
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
try {
|
|
3440
|
+
const legacy = String(tr).trim();
|
|
3441
|
+
cloneRoot.style.transform = legacy + " translate(0px, 0px) rotate(0deg)";
|
|
3442
|
+
return null;
|
|
3443
|
+
} catch {
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
function parseBoxShadow(cs) {
|
|
3448
|
+
const v = cs.boxShadow || "";
|
|
3449
|
+
if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
3450
|
+
const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
|
|
3451
|
+
let t = 0, r = 0, b2 = 0, l = 0;
|
|
3452
|
+
for (const part of parts) {
|
|
3453
|
+
const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
|
|
3454
|
+
if (nums.length < 2) continue;
|
|
3455
|
+
const [ox2, oy2, blur = 0, spread = 0] = nums;
|
|
3456
|
+
const extX = Math.abs(ox2) + blur + spread;
|
|
3457
|
+
const extY = Math.abs(oy2) + blur + spread;
|
|
3458
|
+
r = Math.max(r, extX + Math.max(ox2, 0));
|
|
3459
|
+
l = Math.max(l, extX + Math.max(-ox2, 0));
|
|
3460
|
+
b2 = Math.max(b2, extY + Math.max(oy2, 0));
|
|
3461
|
+
t = Math.max(t, extY + Math.max(-oy2, 0));
|
|
3462
|
+
}
|
|
3463
|
+
return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
|
|
3464
|
+
}
|
|
3465
|
+
function parseFilterBlur(cs) {
|
|
3466
|
+
const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
|
|
3467
|
+
const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
|
|
3468
|
+
return { top: b2, right: b2, bottom: b2, left: b2 };
|
|
3469
|
+
}
|
|
3470
|
+
function parseOutline(cs) {
|
|
3471
|
+
if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
3472
|
+
const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
|
|
3473
|
+
return { top: w2, right: w2, bottom: w2, left: w2 };
|
|
3474
|
+
}
|
|
3475
|
+
function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
|
|
3476
|
+
const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
|
|
3477
|
+
function pt(x, y) {
|
|
3478
|
+
let X = x - ox2, Y = y - oy2;
|
|
3479
|
+
let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
|
|
3480
|
+
X2 += ox2 + e2;
|
|
3481
|
+
Y2 += oy2 + f2;
|
|
3482
|
+
return [X2, Y2];
|
|
3483
|
+
}
|
|
3484
|
+
const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
|
|
3485
|
+
let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
|
|
3486
|
+
for (const [X, Y] of P) {
|
|
3487
|
+
if (X < minX2) minX2 = X;
|
|
3488
|
+
if (Y < minY2) minY2 = Y;
|
|
3489
|
+
if (X > maxX2) maxX2 = X;
|
|
3490
|
+
if (Y > maxY2) maxY2 = Y;
|
|
3491
|
+
}
|
|
3492
|
+
return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
|
|
3493
|
+
}
|
|
3494
|
+
function hasTFBBox(el) {
|
|
3495
|
+
return hasBBoxAffectingTransform(el);
|
|
3496
|
+
}
|
|
3132
3497
|
function matrixFromComputed(el) {
|
|
3133
3498
|
const tr = getComputedStyle(el).transform;
|
|
3134
3499
|
if (!tr || tr === "none") return new DOMMatrix();
|
|
@@ -3281,20 +3646,20 @@ function createContext(options = {}) {
|
|
|
3281
3646
|
excludeMode: options.excludeMode ?? "hide",
|
|
3282
3647
|
filter: options.filter ?? null,
|
|
3283
3648
|
filterMode: options.filterMode ?? "hide",
|
|
3649
|
+
// Placeholders
|
|
3284
3650
|
placeholders: options.placeholders !== false,
|
|
3285
3651
|
// default true
|
|
3286
|
-
//
|
|
3652
|
+
// Fonts
|
|
3287
3653
|
embedFonts: options.embedFonts ?? false,
|
|
3288
3654
|
iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
|
|
3289
3655
|
localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
|
|
3290
3656
|
excludeFonts: options.excludeFonts ?? void 0,
|
|
3291
3657
|
fallbackURL: options.fallbackURL ?? void 0,
|
|
3292
|
-
/** Cache policy (disabled|full|auto|soft). Default: "soft" */
|
|
3293
3658
|
/** @type {CachePolicy} */
|
|
3294
3659
|
cache: cachePolicy,
|
|
3295
|
-
//
|
|
3660
|
+
// Network
|
|
3296
3661
|
useProxy: typeof options.useProxy === "string" ? options.useProxy : "",
|
|
3297
|
-
//
|
|
3662
|
+
// Output
|
|
3298
3663
|
width: options.width ?? null,
|
|
3299
3664
|
height: options.height ?? null,
|
|
3300
3665
|
format: resolvedFormat,
|
|
@@ -3302,8 +3667,11 @@ function createContext(options = {}) {
|
|
|
3302
3667
|
quality: options.quality ?? 0.92,
|
|
3303
3668
|
dpr: options.dpr ?? (window.devicePixelRatio || 1),
|
|
3304
3669
|
backgroundColor: options.backgroundColor ?? (["jpg", "jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
|
|
3305
|
-
filename: options.filename ?? "snapDOM"
|
|
3306
|
-
//
|
|
3670
|
+
filename: options.filename ?? "snapDOM",
|
|
3671
|
+
// NEW flags (user-friendly)
|
|
3672
|
+
straighten: options.straighten ?? false,
|
|
3673
|
+
noShadows: options.noShadows ?? false
|
|
3674
|
+
// Plugins (reservado)
|
|
3307
3675
|
// plugins: normalizePlugins(...),
|
|
3308
3676
|
};
|
|
3309
3677
|
}
|
|
@@ -3493,9 +3861,20 @@ async function toImg(url, options) {
|
|
|
3493
3861
|
const k = height / Math.max(1, refH);
|
|
3494
3862
|
img.style.height = `${height}px`;
|
|
3495
3863
|
img.style.width = `${Math.round(refW * k)}px`;
|
|
3496
|
-
} else
|
|
3497
|
-
|
|
3498
|
-
|
|
3864
|
+
} else {
|
|
3865
|
+
const cssW = Math.round(img.naturalWidth * scale);
|
|
3866
|
+
const cssH = Math.round(img.naturalHeight * scale);
|
|
3867
|
+
img.style.width = `${cssW}px`;
|
|
3868
|
+
img.style.height = `${cssH}px`;
|
|
3869
|
+
if (typeof url === "string" && url.startsWith("data:image/svg+xml")) {
|
|
3870
|
+
try {
|
|
3871
|
+
const decoded = decodeURIComponent(url.split(",")[1]);
|
|
3872
|
+
const patched = decoded.replace(/width="[^"]*"/, `width="${cssW}"`).replace(/height="[^"]*"/, `height="${cssH}"`);
|
|
3873
|
+
url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(patched)}`;
|
|
3874
|
+
img.src = url;
|
|
3875
|
+
} catch {
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3499
3878
|
}
|
|
3500
3879
|
return img;
|
|
3501
3880
|
}
|
|
@@ -3551,7 +3930,7 @@ async function snapdom(element, userOptions) {
|
|
|
3551
3930
|
await safariWarmup(element, userOptions);
|
|
3552
3931
|
console.log("Iteraci\xF3n n\xFAmero:", i);
|
|
3553
3932
|
_safariWarmup = false;
|
|
3554
|
-
} catch
|
|
3933
|
+
} catch {
|
|
3555
3934
|
}
|
|
3556
3935
|
}
|
|
3557
3936
|
}
|
|
@@ -3559,7 +3938,7 @@ async function snapdom(element, userOptions) {
|
|
|
3559
3938
|
if (!context.snap) {
|
|
3560
3939
|
context.snap = {
|
|
3561
3940
|
toPng: (el, opts) => snapdom.toPng(el, opts),
|
|
3562
|
-
|
|
3941
|
+
toSvg: (el, opts) => snapdom.toSvg(el, opts)
|
|
3563
3942
|
};
|
|
3564
3943
|
}
|
|
3565
3944
|
return snapdom.capture(element, context, INTERNAL_TOKEN);
|
|
@@ -3581,6 +3960,7 @@ snapdom.capture = async (el, context, _token) => {
|
|
|
3581
3960
|
url,
|
|
3582
3961
|
toRaw: () => url,
|
|
3583
3962
|
toImg: (opts) => toImg(url, ensureContext(opts)),
|
|
3963
|
+
toSvg: (opts) => toImg(url, ensureContext(opts)),
|
|
3584
3964
|
toCanvas: (opts) => toCanvas(url, ensureContext(opts)),
|
|
3585
3965
|
toBlob: (opts) => toBlob(url, ensureContext(opts)),
|
|
3586
3966
|
toPng: withFormat("png"),
|
|
@@ -3591,6 +3971,7 @@ snapdom.capture = async (el, context, _token) => {
|
|
|
3591
3971
|
};
|
|
3592
3972
|
snapdom.toRaw = (el, options) => snapdom(el, options).then((result) => result.toRaw());
|
|
3593
3973
|
snapdom.toImg = (el, options) => snapdom(el, options).then((result) => result.toImg());
|
|
3974
|
+
snapdom.toSvg = (el, options) => snapdom(el, options).then((result) => result.toSvg());
|
|
3594
3975
|
snapdom.toCanvas = (el, options) => snapdom(el, options).then((result) => result.toCanvas());
|
|
3595
3976
|
snapdom.toBlob = (el, options) => snapdom(el, options).then((result) => result.toBlob());
|
|
3596
3977
|
snapdom.toPng = (el, options) => snapdom(el, { ...options, format: "png" }).then((result) => result.toPng());
|
|
@@ -3729,7 +4110,7 @@ async function preCache(root = document, options = {}) {
|
|
|
3729
4110
|
const families = new Set(
|
|
3730
4111
|
Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
|
|
3731
4112
|
);
|
|
3732
|
-
await ensureFontsReady(families,
|
|
4113
|
+
await ensureFontsReady(families, 3);
|
|
3733
4114
|
}
|
|
3734
4115
|
await embedCustomFonts({
|
|
3735
4116
|
required,
|