@zumer/snapdom 2.0.0-dev.0 → 2.0.0-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 +20 -18
- package/dist/snapdom.js +412 -55
- package/dist/snapdom.min.js +7 -7
- package/dist/snapdom.min.mjs +7 -7
- package/dist/snapdom.mjs +399 -55
- package/package.json +1 -1
- package/types/snapdom.d.ts +3 -3
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.2.0.0-dev.
|
|
4
|
+
* v.2.0.0-dev.2
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -328,19 +328,6 @@ async function snapFetch(url, options = {}) {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
// src/utils/image.js
|
|
331
|
-
function createBackground(baseCanvas, backgroundColor) {
|
|
332
|
-
if (!backgroundColor || !baseCanvas.width || !baseCanvas.height) {
|
|
333
|
-
return baseCanvas;
|
|
334
|
-
}
|
|
335
|
-
const temp = document.createElement("canvas");
|
|
336
|
-
temp.width = baseCanvas.width;
|
|
337
|
-
temp.height = baseCanvas.height;
|
|
338
|
-
const ctx = temp.getContext("2d");
|
|
339
|
-
ctx.fillStyle = backgroundColor;
|
|
340
|
-
ctx.fillRect(0, 0, temp.width, temp.height);
|
|
341
|
-
ctx.drawImage(baseCanvas, 0, 0);
|
|
342
|
-
return temp;
|
|
343
|
-
}
|
|
344
331
|
async function inlineSingleBackgroundEntry(entry, options = {}) {
|
|
345
332
|
const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
|
|
346
333
|
if (isGradient || entry.trim() === "none") {
|
|
@@ -616,11 +603,20 @@ function idle(fn, { fast = false } = {}) {
|
|
|
616
603
|
}
|
|
617
604
|
}
|
|
618
605
|
function isSafari() {
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
|
|
606
|
+
if (typeof navigator === "undefined") return false;
|
|
607
|
+
const ua = navigator.userAgent || "";
|
|
608
|
+
const uaLower = ua.toLowerCase();
|
|
609
|
+
const isSafariUA = uaLower.includes("safari") && !uaLower.includes("chrome") && !uaLower.includes("crios") && // Chrome on iOS
|
|
610
|
+
!uaLower.includes("fxios") && // Firefox on iOS
|
|
611
|
+
!uaLower.includes("android");
|
|
612
|
+
const isWebKit = /applewebkit/i.test(ua);
|
|
613
|
+
const isMobile = /mobile/i.test(ua);
|
|
614
|
+
const missingSafariToken = !/safari/i.test(ua);
|
|
615
|
+
const isUIWebView = isWebKit && isMobile && missingSafariToken;
|
|
616
|
+
const isWeChatUA = /(micromessenger|wxwork|wecom|windowswechat|macwechat)/i.test(ua);
|
|
617
|
+
const isBaiduUA = /(baiduboxapp|baidubrowser|baidusearch|baiduboxlite)/i.test(uaLower);
|
|
618
|
+
const isIOSWebKit = /ipad|iphone|ipod/.test(uaLower) && isWebKit;
|
|
619
|
+
return isSafariUA || isUIWebView || isWeChatUA || isBaiduUA || isIOSWebKit;
|
|
624
620
|
}
|
|
625
621
|
|
|
626
622
|
// src/modules/styles.js
|
|
@@ -665,6 +661,22 @@ function snapshotComputedStyleFull(style, options = {}) {
|
|
|
665
661
|
}
|
|
666
662
|
out[prop] = val;
|
|
667
663
|
}
|
|
664
|
+
const EXTRA_TEXT_DECORATION_PROPS = [
|
|
665
|
+
"text-decoration-line",
|
|
666
|
+
"text-decoration-color",
|
|
667
|
+
"text-decoration-style",
|
|
668
|
+
"text-decoration-thickness",
|
|
669
|
+
"text-underline-offset",
|
|
670
|
+
"text-decoration-skip-ink"
|
|
671
|
+
];
|
|
672
|
+
for (const prop of EXTRA_TEXT_DECORATION_PROPS) {
|
|
673
|
+
if (out[prop]) continue;
|
|
674
|
+
try {
|
|
675
|
+
const v = style.getPropertyValue(prop);
|
|
676
|
+
if (v) out[prop] = v;
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
668
680
|
if (options.embedFonts) {
|
|
669
681
|
const EXTRA_FONT_PROPS = [
|
|
670
682
|
"font-feature-settings",
|
|
@@ -1421,17 +1433,19 @@ var defaultIconFonts = [
|
|
|
1421
1433
|
/layui/i,
|
|
1422
1434
|
/lucide/i
|
|
1423
1435
|
];
|
|
1436
|
+
var ICON_FONT_URLS = Object.assign({
|
|
1437
|
+
materialIconsFilled: "https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2",
|
|
1438
|
+
materialIconsOutlined: "https://fonts.gstatic.com/s/materialiconsoutlined/v110/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUcel5euIg.woff2",
|
|
1439
|
+
materialIconsRound: "https://fonts.gstatic.com/s/materialiconsround/v109/LDItaoyNOAY6Uewc665JcIzCKsKc_M9flwmPq_HTTw.woff2",
|
|
1440
|
+
materialIconsSharp: "https://fonts.gstatic.com/s/materialiconssharp/v110/oPWQ_lt5nv4pWNJpghLP75WiFR4kLh3kvmvRImcycg.woff2"
|
|
1441
|
+
}, typeof window !== "undefined" && window.__SNAPDOM_ICON_FONTS__ || {});
|
|
1424
1442
|
var userIconFonts = [];
|
|
1425
1443
|
function extendIconFonts(fonts) {
|
|
1426
1444
|
const list = Array.isArray(fonts) ? fonts : [fonts];
|
|
1427
1445
|
for (const f of list) {
|
|
1428
|
-
if (f instanceof RegExp)
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
userIconFonts.push(new RegExp(f, "i"));
|
|
1432
|
-
} else {
|
|
1433
|
-
console.warn("[snapdom] Ignored invalid iconFont value:", f);
|
|
1434
|
-
}
|
|
1446
|
+
if (f instanceof RegExp) userIconFonts.push(f);
|
|
1447
|
+
else if (typeof f === "string") userIconFonts.push(new RegExp(f, "i"));
|
|
1448
|
+
else console.warn("[snapdom] Ignored invalid iconFont value:", f);
|
|
1435
1449
|
}
|
|
1436
1450
|
}
|
|
1437
1451
|
function isIconFont2(input) {
|
|
@@ -1443,6 +1457,177 @@ function isIconFont2(input) {
|
|
|
1443
1457
|
if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
|
|
1444
1458
|
return false;
|
|
1445
1459
|
}
|
|
1460
|
+
function isMaterialFamily(family = "") {
|
|
1461
|
+
const s = String(family).toLowerCase();
|
|
1462
|
+
return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
|
|
1463
|
+
}
|
|
1464
|
+
var loadedCanvasFamilies = /* @__PURE__ */ new Map();
|
|
1465
|
+
function parseAxes(variation = "") {
|
|
1466
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1467
|
+
const v = String(variation || "");
|
|
1468
|
+
const rx = /['"]?\s*([A-Za-z]{3,4})\s*['"]?\s*([+-]?\d+(?:\.\d+)?)\s*/g;
|
|
1469
|
+
let m;
|
|
1470
|
+
while (m = rx.exec(v)) out[m[1].toUpperCase()] = Number(m[2]);
|
|
1471
|
+
return out;
|
|
1472
|
+
}
|
|
1473
|
+
async function ensureLigatureCanvasFont(cssFamily, className, axes) {
|
|
1474
|
+
const fam = String(cssFamily || "");
|
|
1475
|
+
const lowerFam = fam.toLowerCase();
|
|
1476
|
+
const cls = String(className || "").toLowerCase();
|
|
1477
|
+
if (/\bmaterial\s*icons\b/.test(lowerFam) && !/\bsymbols\b/.test(lowerFam)) {
|
|
1478
|
+
return { familyForMeasure: fam, familyForCanvas: fam };
|
|
1479
|
+
}
|
|
1480
|
+
const isSymbols = /\bmaterial\s*symbols\b/.test(lowerFam);
|
|
1481
|
+
if (!isSymbols) {
|
|
1482
|
+
return { familyForMeasure: fam, familyForCanvas: fam };
|
|
1483
|
+
}
|
|
1484
|
+
const FILL = axes && (axes.FILL ?? axes.fill);
|
|
1485
|
+
let style = "outlined";
|
|
1486
|
+
if (/\brounded\b/.test(cls) || /\bround\b/.test(cls)) style = "rounded";
|
|
1487
|
+
else if (/\bsharp\b/.test(cls)) style = "sharp";
|
|
1488
|
+
else if (/\boutlined\b/.test(cls)) style = "outlined";
|
|
1489
|
+
const filled = FILL === 1;
|
|
1490
|
+
let pick = null;
|
|
1491
|
+
if (filled) {
|
|
1492
|
+
if (style === "outlined" && ICON_FONT_URLS.materialIconsFilled) {
|
|
1493
|
+
pick = { url: ICON_FONT_URLS.materialIconsFilled, alias: "snapdom-mi-filled" };
|
|
1494
|
+
} else if (style === "rounded" && ICON_FONT_URLS.materialIconsRound) {
|
|
1495
|
+
pick = { url: ICON_FONT_URLS.materialIconsRound, alias: "snapdom-mi-round" };
|
|
1496
|
+
} else if (style === "sharp" && ICON_FONT_URLS.materialIconsSharp) {
|
|
1497
|
+
pick = { url: ICON_FONT_URLS.materialIconsSharp, alias: "snapdom-mi-sharp" };
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (!pick) {
|
|
1501
|
+
return { familyForMeasure: fam, familyForCanvas: fam };
|
|
1502
|
+
}
|
|
1503
|
+
if (!loadedCanvasFamilies.has(pick.alias)) {
|
|
1504
|
+
try {
|
|
1505
|
+
const ff = new FontFace(pick.alias, `url(${pick.url})`, { style: "normal", weight: "400" });
|
|
1506
|
+
document.fonts.add(ff);
|
|
1507
|
+
await ff.load();
|
|
1508
|
+
loadedCanvasFamilies.set(pick.alias, true);
|
|
1509
|
+
} catch {
|
|
1510
|
+
return { familyForMeasure: fam, familyForCanvas: fam };
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const quoted = `"${pick.alias}"`;
|
|
1514
|
+
return { familyForMeasure: quoted, familyForCanvas: quoted };
|
|
1515
|
+
}
|
|
1516
|
+
async function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
|
|
1517
|
+
try {
|
|
1518
|
+
await Promise.all([
|
|
1519
|
+
document.fonts.load(`400 ${px}px "${String(family).replace(/["']/g, "")}"`),
|
|
1520
|
+
document.fonts.ready
|
|
1521
|
+
]);
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function resolvePaintColor(cs) {
|
|
1526
|
+
let fill = cs.getPropertyValue("-webkit-text-fill-color")?.trim() || "";
|
|
1527
|
+
const isTransparent = /^transparent$/i.test(fill) || /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(fill);
|
|
1528
|
+
if (fill && !isTransparent && fill.toLowerCase() !== "currentcolor") return fill;
|
|
1529
|
+
const c = cs.color?.trim();
|
|
1530
|
+
return c && c !== "inherit" ? c : "#000";
|
|
1531
|
+
}
|
|
1532
|
+
async function materialIconToImage(ligatureText, {
|
|
1533
|
+
family = "Material Icons",
|
|
1534
|
+
weight = "normal",
|
|
1535
|
+
fontSize = 32,
|
|
1536
|
+
color = "#000",
|
|
1537
|
+
variation = "",
|
|
1538
|
+
className = ""
|
|
1539
|
+
} = {}) {
|
|
1540
|
+
const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
|
|
1541
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1542
|
+
const axes = parseAxes(variation);
|
|
1543
|
+
const { familyForMeasure, familyForCanvas } = await ensureLigatureCanvasFont(fam, className, axes);
|
|
1544
|
+
await ensureMaterialFontsReady(familyForCanvas.replace(/^["']+|["']+$/g, ""), fontSize);
|
|
1545
|
+
const span = document.createElement("span");
|
|
1546
|
+
span.textContent = ligatureText;
|
|
1547
|
+
span.style.position = "absolute";
|
|
1548
|
+
span.style.visibility = "hidden";
|
|
1549
|
+
span.style.left = "-99999px";
|
|
1550
|
+
span.style.whiteSpace = "nowrap";
|
|
1551
|
+
span.style.fontFamily = familyForMeasure;
|
|
1552
|
+
span.style.fontWeight = String(weight || "normal");
|
|
1553
|
+
span.style.fontSize = `${fontSize}px`;
|
|
1554
|
+
span.style.lineHeight = "1";
|
|
1555
|
+
span.style.margin = "0";
|
|
1556
|
+
span.style.padding = "0";
|
|
1557
|
+
span.style.fontFeatureSettings = "'liga' 1";
|
|
1558
|
+
span.style.fontVariantLigatures = "normal";
|
|
1559
|
+
span.style.color = color;
|
|
1560
|
+
document.body.appendChild(span);
|
|
1561
|
+
const rect = span.getBoundingClientRect();
|
|
1562
|
+
const width = Math.max(1, Math.ceil(rect.width));
|
|
1563
|
+
const height = Math.max(1, Math.ceil(rect.height));
|
|
1564
|
+
document.body.removeChild(span);
|
|
1565
|
+
const canvas = document.createElement("canvas");
|
|
1566
|
+
canvas.width = width * dpr;
|
|
1567
|
+
canvas.height = height * dpr;
|
|
1568
|
+
const ctx = canvas.getContext("2d");
|
|
1569
|
+
ctx.scale(dpr, dpr);
|
|
1570
|
+
ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px ${familyForCanvas}`;
|
|
1571
|
+
ctx.textAlign = "left";
|
|
1572
|
+
ctx.textBaseline = "top";
|
|
1573
|
+
ctx.fillStyle = color;
|
|
1574
|
+
try {
|
|
1575
|
+
ctx.fontKerning = "normal";
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
ctx.fillText(ligatureText, 0, 0);
|
|
1579
|
+
return {
|
|
1580
|
+
dataUrl: canvas.toDataURL(),
|
|
1581
|
+
width,
|
|
1582
|
+
height
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
async function ligatureIconToImage(cloneRoot, sourceRoot) {
|
|
1586
|
+
if (!(cloneRoot instanceof Element)) return 0;
|
|
1587
|
+
const selector = '.material-icons, [class*="material-symbols"]';
|
|
1588
|
+
const cloneNodes = Array.from(
|
|
1589
|
+
cloneRoot.querySelectorAll(selector)
|
|
1590
|
+
).filter((n) => n && n.textContent && n.textContent.trim());
|
|
1591
|
+
if (cloneNodes.length === 0) return 0;
|
|
1592
|
+
const sourceNodes = sourceRoot instanceof Element ? Array.from(sourceRoot.querySelectorAll(selector)).filter((n) => n && n.textContent && n.textContent.trim()) : [];
|
|
1593
|
+
let replaced = 0;
|
|
1594
|
+
for (let i = 0; i < cloneNodes.length; i++) {
|
|
1595
|
+
const el = cloneNodes[i];
|
|
1596
|
+
const src = sourceNodes[i] || null;
|
|
1597
|
+
try {
|
|
1598
|
+
const cs = src ? getComputedStyle(src) : getComputedStyle(el);
|
|
1599
|
+
const family = cs.fontFamily || "Material Icons";
|
|
1600
|
+
if (!isMaterialFamily(family)) continue;
|
|
1601
|
+
const text = (src || el).textContent.trim();
|
|
1602
|
+
if (!text) continue;
|
|
1603
|
+
const size = parseInt(cs.fontSize, 10) || 24;
|
|
1604
|
+
const weight = cs.fontWeight && cs.fontWeight !== "normal" ? cs.fontWeight : "normal";
|
|
1605
|
+
const color = resolvePaintColor(cs);
|
|
1606
|
+
const variation = cs.fontVariationSettings && cs.fontVariationSettings !== "normal" ? cs.fontVariationSettings : "";
|
|
1607
|
+
const className = (src || el).className || "";
|
|
1608
|
+
const { dataUrl, width, height } = await materialIconToImage(text, {
|
|
1609
|
+
family,
|
|
1610
|
+
weight,
|
|
1611
|
+
fontSize: size,
|
|
1612
|
+
color,
|
|
1613
|
+
variation,
|
|
1614
|
+
className
|
|
1615
|
+
});
|
|
1616
|
+
el.textContent = "";
|
|
1617
|
+
const img = el.ownerDocument.createElement("img");
|
|
1618
|
+
img.src = dataUrl;
|
|
1619
|
+
img.alt = text;
|
|
1620
|
+
img.style.height = `${size}px`;
|
|
1621
|
+
img.style.width = `${Math.max(1, Math.round(width / height * size))}px`;
|
|
1622
|
+
img.style.objectFit = "contain";
|
|
1623
|
+
img.style.verticalAlign = getComputedStyle(el).verticalAlign || "baseline";
|
|
1624
|
+
el.appendChild(img);
|
|
1625
|
+
replaced++;
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return replaced;
|
|
1630
|
+
}
|
|
1446
1631
|
|
|
1447
1632
|
// src/modules/fonts.js
|
|
1448
1633
|
async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
|
|
@@ -1792,7 +1977,7 @@ function dedupeFontFaces(cssText) {
|
|
|
1792
1977
|
const styleSpec = (block.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
1793
1978
|
const stretchSpec = (block.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
1794
1979
|
const urange = (block.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
1795
|
-
const srcRaw = (block.match(/src\s*:\s*([^;]+)
|
|
1980
|
+
const srcRaw = (block.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
|
|
1796
1981
|
const urls = extractSrcUrls(srcRaw, location.href);
|
|
1797
1982
|
const srcPart = urls.length ? urls.map((u) => String(u).toLowerCase()).sort().join("|") : srcRaw.toLowerCase();
|
|
1798
1983
|
const key = [
|
|
@@ -2000,7 +2185,7 @@ async function embedCustomFonts({
|
|
|
2000
2185
|
const styleSpec = (face.match(/font-style:\s*([^;]+);/i)?.[1] || "normal").trim();
|
|
2001
2186
|
const stretchSpec = (face.match(/font-stretch:\s*([^;]+);/i)?.[1] || "100%").trim();
|
|
2002
2187
|
const urange = (face.match(/unicode-range:\s*([^;]+);/i)?.[1] || "").trim();
|
|
2003
|
-
const srcRaw = (face.match(/src\s*:\s*([^;]+)
|
|
2188
|
+
const srcRaw = (face.match(/src\s*:\s*([^;}]+)[;}]/i)?.[1] || "").trim();
|
|
2004
2189
|
const srcUrls = extractSrcUrls(srcRaw, link.href);
|
|
2005
2190
|
if (!faceMatchesRequired(family, styleSpec, weightSpec, stretchSpec)) continue;
|
|
2006
2191
|
const ranges = parseUnicodeRange(urange);
|
|
@@ -3442,12 +3627,56 @@ function stripRootShadows(originalEl, cloneRoot) {
|
|
|
3442
3627
|
} catch {
|
|
3443
3628
|
}
|
|
3444
3629
|
}
|
|
3630
|
+
function removeAllComments(root) {
|
|
3631
|
+
const it = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
3632
|
+
const toRemove = [];
|
|
3633
|
+
while (it.nextNode()) toRemove.push(it.currentNode);
|
|
3634
|
+
for (const n of toRemove) n.remove();
|
|
3635
|
+
}
|
|
3636
|
+
function sanitizeAttributesForXHTML(root, opts = {}) {
|
|
3637
|
+
const { stripFrameworkDirectives = true } = opts;
|
|
3638
|
+
const ALLOWED_PREFIXES = /* @__PURE__ */ new Set(["xml", "xlink"]);
|
|
3639
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
3640
|
+
while (walker.nextNode()) {
|
|
3641
|
+
const el = walker.currentNode;
|
|
3642
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3643
|
+
const name = attr.name;
|
|
3644
|
+
if (name.includes("@")) {
|
|
3645
|
+
el.removeAttribute(name);
|
|
3646
|
+
continue;
|
|
3647
|
+
}
|
|
3648
|
+
if (name.includes(":")) {
|
|
3649
|
+
const prefix = name.split(":", 1)[0];
|
|
3650
|
+
if (!ALLOWED_PREFIXES.has(prefix)) {
|
|
3651
|
+
el.removeAttribute(name);
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
if (!stripFrameworkDirectives) continue;
|
|
3656
|
+
if (name.startsWith("x-") || // Alpine
|
|
3657
|
+
name.startsWith("v-") || // Vue
|
|
3658
|
+
name.startsWith(":") || // Vue/Alpine shorthand
|
|
3659
|
+
name.startsWith("on:") || // Svelte
|
|
3660
|
+
name.startsWith("bind:") || // Svelte
|
|
3661
|
+
name.startsWith("let:") || // Svelte
|
|
3662
|
+
name.startsWith("class:")) {
|
|
3663
|
+
el.removeAttribute(name);
|
|
3664
|
+
continue;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
function sanitizeCloneForXHTML(root, opts = {}) {
|
|
3670
|
+
if (!root) return;
|
|
3671
|
+
sanitizeAttributesForXHTML(root, opts);
|
|
3672
|
+
removeAllComments(root);
|
|
3673
|
+
}
|
|
3445
3674
|
async function captureDOM(element, options) {
|
|
3446
3675
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
3447
3676
|
applyCachePolicy(options.cache);
|
|
3448
3677
|
const fast = options.fast;
|
|
3449
|
-
const
|
|
3450
|
-
const
|
|
3678
|
+
const outerTransforms = options.outerTransforms !== false;
|
|
3679
|
+
const outerShadows = !!options.outerShadows;
|
|
3451
3680
|
let state = { element, options, plugins: options.plugins };
|
|
3452
3681
|
let clone, classCSS, styleCache;
|
|
3453
3682
|
let fontsCSS = "";
|
|
@@ -3460,17 +3689,29 @@ async function captureDOM(element, options) {
|
|
|
3460
3689
|
const undoClamp = lineClamp(state.element);
|
|
3461
3690
|
try {
|
|
3462
3691
|
({ clone, classCSS, styleCache } = await prepareClone(state.element, state.options));
|
|
3463
|
-
if (
|
|
3464
|
-
rootTransform2D = normalizeRootTransforms(state.element,
|
|
3692
|
+
if (!outerTransforms && clone) {
|
|
3693
|
+
rootTransform2D = normalizeRootTransforms(state.element, clone);
|
|
3465
3694
|
}
|
|
3466
|
-
if (
|
|
3467
|
-
stripRootShadows(state.element,
|
|
3695
|
+
if (!outerShadows && clone) {
|
|
3696
|
+
stripRootShadows(state.element, clone);
|
|
3468
3697
|
}
|
|
3469
3698
|
} finally {
|
|
3470
3699
|
undoClamp();
|
|
3471
3700
|
}
|
|
3472
3701
|
state = { clone, classCSS, styleCache, ...state };
|
|
3473
3702
|
await runHook("afterClone", state);
|
|
3703
|
+
sanitizeCloneForXHTML(state.clone);
|
|
3704
|
+
if (state.options?.excludeMode === "remove") {
|
|
3705
|
+
try {
|
|
3706
|
+
shrinkAutoSizeBoxes(state.element, state.clone, state.styleCache);
|
|
3707
|
+
} catch (e) {
|
|
3708
|
+
console.warn("[snapdom] shrink pass failed:", e);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
try {
|
|
3712
|
+
await ligatureIconToImage(state.clone, state.element);
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3474
3715
|
await new Promise((resolve) => {
|
|
3475
3716
|
idle(async () => {
|
|
3476
3717
|
await inlineImages(state.clone, state.options);
|
|
@@ -3524,8 +3765,15 @@ async function captureDOM(element, options) {
|
|
|
3524
3765
|
idle(() => {
|
|
3525
3766
|
const csEl = getComputedStyle(state.element);
|
|
3526
3767
|
const rect = state.element.getBoundingClientRect();
|
|
3527
|
-
|
|
3528
|
-
|
|
3768
|
+
let w0 = Math.max(1, limitDecimals(state.element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
|
|
3769
|
+
let h0 = Math.max(1, limitDecimals(state.element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
|
|
3770
|
+
if (state.options?.excludeMode === "remove") {
|
|
3771
|
+
const hEst = estimateKeptHeight(state.element, state.options);
|
|
3772
|
+
const EPS = 1;
|
|
3773
|
+
if (Number.isFinite(hEst) && hEst > 0) {
|
|
3774
|
+
h0 = Math.max(1, Math.min(h0, limitDecimals(hEst + EPS)));
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3529
3777
|
const coerceNum = (v, def = NaN) => {
|
|
3530
3778
|
const n = typeof v === "string" ? parseFloat(v) : v;
|
|
3531
3779
|
return Number.isFinite(n) ? n : def;
|
|
@@ -3550,7 +3798,7 @@ async function captureDOM(element, options) {
|
|
|
3550
3798
|
h = h0;
|
|
3551
3799
|
}
|
|
3552
3800
|
let minX = 0, minY = 0, maxX = w0, maxY = h0;
|
|
3553
|
-
if (
|
|
3801
|
+
if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
|
|
3554
3802
|
const M2 = {
|
|
3555
3803
|
a: rootTransform2D.a,
|
|
3556
3804
|
b: rootTransform2D.b || 0,
|
|
@@ -3565,7 +3813,7 @@ async function captureDOM(element, options) {
|
|
|
3565
3813
|
maxX = limitDecimals(bb2.maxX);
|
|
3566
3814
|
maxY = limitDecimals(bb2.maxY);
|
|
3567
3815
|
} else {
|
|
3568
|
-
const useTFBBox =
|
|
3816
|
+
const useTFBBox = outerTransforms && hasTFBBox(state.element);
|
|
3569
3817
|
if (useTFBBox) {
|
|
3570
3818
|
const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
|
|
3571
3819
|
const ind2 = readIndividualTransforms(state.element);
|
|
@@ -3588,7 +3836,7 @@ async function captureDOM(element, options) {
|
|
|
3588
3836
|
const bleedBlur = parseFilterBlur(csEl);
|
|
3589
3837
|
const bleedOutline = parseOutline(csEl);
|
|
3590
3838
|
const drop = parseFilterDropShadows(csEl);
|
|
3591
|
-
const bleed =
|
|
3839
|
+
const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
|
|
3592
3840
|
top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
|
|
3593
3841
|
right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
|
|
3594
3842
|
bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
|
|
@@ -3606,7 +3854,7 @@ async function captureDOM(element, options) {
|
|
|
3606
3854
|
const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
|
|
3607
3855
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
3608
3856
|
const basePad = isSafari() ? 1 : 0;
|
|
3609
|
-
const extraPad =
|
|
3857
|
+
const extraPad = !outerTransforms ? 1 : 0;
|
|
3610
3858
|
const pad = limitDecimals(basePad + extraPad);
|
|
3611
3859
|
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
3612
3860
|
const vbMinX = limitDecimals(minX);
|
|
@@ -3648,6 +3896,57 @@ async function captureDOM(element, options) {
|
|
|
3648
3896
|
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
3649
3897
|
return state.dataURL;
|
|
3650
3898
|
}
|
|
3899
|
+
function authorHasExplicitSize(el) {
|
|
3900
|
+
try {
|
|
3901
|
+
const s = el.getAttribute?.("style") || "";
|
|
3902
|
+
return /\b(height|width|block-size|inline-size)\s*:/.test(s);
|
|
3903
|
+
} catch {
|
|
3904
|
+
return false;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
function isReplacedElement(el) {
|
|
3908
|
+
return el instanceof HTMLImageElement || el instanceof HTMLCanvasElement || el instanceof HTMLVideoElement || el instanceof HTMLIFrameElement || el instanceof SVGElement || el instanceof HTMLObjectElement || el instanceof HTMLEmbedElement;
|
|
3909
|
+
}
|
|
3910
|
+
function shouldShrinkBox(srcEl, cs) {
|
|
3911
|
+
if (!(srcEl instanceof Element)) return false;
|
|
3912
|
+
if (authorHasExplicitSize(srcEl)) return false;
|
|
3913
|
+
if (isReplacedElement(srcEl)) return false;
|
|
3914
|
+
const pos = cs.position;
|
|
3915
|
+
if (pos === "absolute" || pos === "fixed" || pos === "sticky") return false;
|
|
3916
|
+
const disp = cs.display || "";
|
|
3917
|
+
if (disp.includes("flex") || disp.includes("grid") || disp.startsWith("table")) return false;
|
|
3918
|
+
if (cs.transform && cs.transform !== "none") return false;
|
|
3919
|
+
return true;
|
|
3920
|
+
}
|
|
3921
|
+
function shrinkAutoSizeBoxes(sourceRoot, cloneRoot, styleCache = /* @__PURE__ */ new Map()) {
|
|
3922
|
+
function walk(src, cln) {
|
|
3923
|
+
if (!(src instanceof Element) || !(cln instanceof Element)) return;
|
|
3924
|
+
const lostKids = src.childElementCount > cln.childElementCount;
|
|
3925
|
+
const cs = styleCache.get(src) || getComputedStyle(src);
|
|
3926
|
+
if (!styleCache.has(src)) styleCache.set(src, cs);
|
|
3927
|
+
if (lostKids && shouldShrinkBox(src, cs)) {
|
|
3928
|
+
if (!cln.style.height) cln.style.height = "auto";
|
|
3929
|
+
if (!cln.style.width) cln.style.width = "auto";
|
|
3930
|
+
cln.style.removeProperty("block-size");
|
|
3931
|
+
cln.style.removeProperty("inline-size");
|
|
3932
|
+
if (!cln.style.minHeight) cln.style.minHeight = "0";
|
|
3933
|
+
if (!cln.style.minWidth) cln.style.minWidth = "0";
|
|
3934
|
+
if (!cln.style.maxHeight) cln.style.maxHeight = "none";
|
|
3935
|
+
if (!cln.style.maxWidth) cln.style.maxWidth = "none";
|
|
3936
|
+
const oy = cs.overflowY || cs.overflowBlock || "visible";
|
|
3937
|
+
const ox = cs.overflowX || cs.overflowInline || "visible";
|
|
3938
|
+
if (oy !== "visible" || ox !== "visible") {
|
|
3939
|
+
cln.style.overflow = "visible";
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
const sKids = Array.from(src.children);
|
|
3943
|
+
const cKids = Array.from(cln.children);
|
|
3944
|
+
for (let i = 0; i < Math.min(sKids.length, cKids.length); i++) {
|
|
3945
|
+
walk(sKids[i], cKids[i]);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
walk(sourceRoot, cloneRoot);
|
|
3949
|
+
}
|
|
3651
3950
|
var limitDecimals = (v, n = 3) => Number.isFinite(v) ? Math.round(v * 10 ** n) / 10 ** n : v;
|
|
3652
3951
|
function parseFilterDropShadows(cs) {
|
|
3653
3952
|
const raw = `${cs.filter || ""} ${cs.webkitFilter || ""}`.trim();
|
|
@@ -3914,6 +4213,52 @@ function readTotalTransformMatrix(t) {
|
|
|
3914
4213
|
host.removeChild(tmp);
|
|
3915
4214
|
return M;
|
|
3916
4215
|
}
|
|
4216
|
+
function isInNormalFlow(el) {
|
|
4217
|
+
const cs = getComputedStyle(el);
|
|
4218
|
+
if (cs.display === "none") return false;
|
|
4219
|
+
if (cs.position === "absolute" || cs.position === "fixed" || cs.position === "sticky") return false;
|
|
4220
|
+
if ((cs.cssFloat || cs.float || "none") !== "none") return false;
|
|
4221
|
+
if (cs.transform && cs.transform !== "none") return false;
|
|
4222
|
+
return true;
|
|
4223
|
+
}
|
|
4224
|
+
function willBeExcluded(el, options) {
|
|
4225
|
+
if (!(el instanceof Element)) return false;
|
|
4226
|
+
if (el.getAttribute("data-capture") === "exclude" && options?.excludeMode === "remove") return true;
|
|
4227
|
+
if (Array.isArray(options?.exclude)) {
|
|
4228
|
+
for (const sel of options.exclude) {
|
|
4229
|
+
try {
|
|
4230
|
+
if (el.matches(sel)) return options.excludeMode === "remove";
|
|
4231
|
+
} catch {
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
return false;
|
|
4236
|
+
}
|
|
4237
|
+
function estimateKeptHeight(container, options) {
|
|
4238
|
+
const csC = getComputedStyle(container);
|
|
4239
|
+
const rC = container.getBoundingClientRect();
|
|
4240
|
+
let minTop = Infinity;
|
|
4241
|
+
let maxBottom = -Infinity;
|
|
4242
|
+
let found = false;
|
|
4243
|
+
const kids = Array.from(container.children);
|
|
4244
|
+
for (const k of kids) {
|
|
4245
|
+
if (willBeExcluded(k, options)) continue;
|
|
4246
|
+
if (!isInNormalFlow(k)) continue;
|
|
4247
|
+
const rk = k.getBoundingClientRect();
|
|
4248
|
+
const top = rk.top - rC.top;
|
|
4249
|
+
const bottom = rk.bottom - rC.top;
|
|
4250
|
+
if (bottom <= top) continue;
|
|
4251
|
+
if (top < minTop) minTop = top;
|
|
4252
|
+
if (bottom > maxBottom) maxBottom = bottom;
|
|
4253
|
+
found = true;
|
|
4254
|
+
}
|
|
4255
|
+
const contentSpan = found ? Math.max(0, maxBottom - minTop) : 0;
|
|
4256
|
+
const bt = parseFloat(csC.borderTopWidth) || 0;
|
|
4257
|
+
const bb = parseFloat(csC.borderBottomWidth) || 0;
|
|
4258
|
+
const pt = parseFloat(csC.paddingTop) || 0;
|
|
4259
|
+
const pb = parseFloat(csC.paddingBottom) || 0;
|
|
4260
|
+
return bt + bb + pt + pb + contentSpan;
|
|
4261
|
+
}
|
|
3917
4262
|
|
|
3918
4263
|
// src/core/context.js
|
|
3919
4264
|
function normalizeCachePolicy(v) {
|
|
@@ -3963,8 +4308,8 @@ function createContext(options = {}) {
|
|
|
3963
4308
|
backgroundColor: options.backgroundColor ?? (["jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null),
|
|
3964
4309
|
filename: options.filename ?? "snapDOM",
|
|
3965
4310
|
// NEW flags (user-friendly)
|
|
3966
|
-
|
|
3967
|
-
|
|
4311
|
+
outerTransforms: options.outerTransforms ?? true,
|
|
4312
|
+
outerShadows: options.outerShadows ?? false
|
|
3968
4313
|
// Plugins (reservado)
|
|
3969
4314
|
// plugins: normalizePlugins(...),
|
|
3970
4315
|
};
|
|
@@ -4070,7 +4415,7 @@ function maybeConvertBoxShadowForSafari(url) {
|
|
|
4070
4415
|
}
|
|
4071
4416
|
}
|
|
4072
4417
|
async function toCanvas(url, options) {
|
|
4073
|
-
let { width: optW, height: optH, scale = 1, dpr = 1, meta = {} } = options;
|
|
4418
|
+
let { width: optW, height: optH, scale = 1, dpr = 1, meta = {}, backgroundColor } = options;
|
|
4074
4419
|
url = maybeConvertBoxShadowForSafari(url);
|
|
4075
4420
|
const img = new Image();
|
|
4076
4421
|
img.loading = "eager";
|
|
@@ -4109,6 +4454,12 @@ async function toCanvas(url, options) {
|
|
|
4109
4454
|
canvas.style.height = `${outH}px`;
|
|
4110
4455
|
const ctx = canvas.getContext("2d");
|
|
4111
4456
|
if (dpr !== 1) ctx.scale(dpr, dpr);
|
|
4457
|
+
if (backgroundColor) {
|
|
4458
|
+
ctx.save();
|
|
4459
|
+
ctx.fillStyle = backgroundColor;
|
|
4460
|
+
ctx.fillRect(0, 0, outW, outH);
|
|
4461
|
+
ctx.restore();
|
|
4462
|
+
}
|
|
4112
4463
|
ctx.drawImage(img, 0, 0, outW, outH);
|
|
4113
4464
|
return canvas;
|
|
4114
4465
|
}
|
|
@@ -4116,12 +4467,11 @@ async function toCanvas(url, options) {
|
|
|
4116
4467
|
// src/modules/rasterize.js
|
|
4117
4468
|
async function rasterize(url, options) {
|
|
4118
4469
|
const canvas = await toCanvas(url, options);
|
|
4119
|
-
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
4120
4470
|
const img = new Image();
|
|
4121
|
-
img.src =
|
|
4471
|
+
img.src = canvas.toDataURL(`image/${options.format}`, options.quality);
|
|
4122
4472
|
await img.decode();
|
|
4123
|
-
img.style.width = `${
|
|
4124
|
-
img.style.height = `${
|
|
4473
|
+
img.style.width = `${canvas.width / options.dpr}px`;
|
|
4474
|
+
img.style.height = `${canvas.height / options.dpr}px`;
|
|
4125
4475
|
return img;
|
|
4126
4476
|
}
|
|
4127
4477
|
|
|
@@ -4181,13 +4531,8 @@ async function toBlob(url, options) {
|
|
|
4181
4531
|
return new Blob([svgText], { type: "image/svg+xml" });
|
|
4182
4532
|
}
|
|
4183
4533
|
const canvas = await toCanvas(url, options);
|
|
4184
|
-
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
4185
4534
|
return new Promise(
|
|
4186
|
-
(resolve) =>
|
|
4187
|
-
(blob) => resolve(blob),
|
|
4188
|
-
`image/${type}`,
|
|
4189
|
-
options.quality
|
|
4190
|
-
)
|
|
4535
|
+
(resolve) => canvas.toBlob((blob) => resolve(blob), `image/${type}`, options.quality)
|
|
4191
4536
|
);
|
|
4192
4537
|
}
|
|
4193
4538
|
|
|
@@ -4205,9 +4550,8 @@ async function download(url, options) {
|
|
|
4205
4550
|
return;
|
|
4206
4551
|
}
|
|
4207
4552
|
const canvas = await toCanvas(url, options);
|
|
4208
|
-
const finalCanvas = options.backgroundColor ? createBackground(canvas, options.backgroundColor) : canvas;
|
|
4209
4553
|
const a = document.createElement("a");
|
|
4210
|
-
a.href =
|
|
4554
|
+
a.href = canvas.toDataURL(`image/${options.format}`, options.quality);
|
|
4211
4555
|
a.download = options.filename;
|
|
4212
4556
|
a.click();
|
|
4213
4557
|
}
|
package/package.json
CHANGED
package/types/snapdom.d.ts
CHANGED
|
@@ -78,13 +78,13 @@ export interface SnapdomOptions {
|
|
|
78
78
|
/** How to apply `filter` ("hide" or "remove"). Default "hide". */
|
|
79
79
|
filterMode?: "hide" | "remove";
|
|
80
80
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
81
|
+
/** outerTransforms the root: remove translate/rotate, keep scale/skew. */
|
|
82
|
+
outerTransforms?: boolean;
|
|
83
83
|
/**
|
|
84
84
|
* Do not expand root bbox for shadows/blur/outline; also strip shadows/outline
|
|
85
85
|
* from the cloned root to get a tight capture box.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
outerShadows?: boolean;
|
|
88
88
|
|
|
89
89
|
/** Inline non-icon fonts actually used within the subtree. */
|
|
90
90
|
embedFonts?: boolean;
|