dom-to-pptx 1.0.7 → 1.0.9
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/dom-to-pptx.bundle.js +306 -55
- package/dist/dom-to-pptx.cjs +306 -55
- package/dist/dom-to-pptx.min.js +306 -55
- package/dist/dom-to-pptx.mjs +306 -55
- package/package.json +1 -1
- package/src/index.js +117 -15
- package/src/utils.js +128 -41
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.9] - 2025-12-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Linear Gradients not applied properly**: Seperated the logic to handle tailwind class and normal css.
|
|
10
|
+
- **Font Awesome icon library cannot be converted**: Updated logic to handle Icons inside lists and span elements. fixes [#3].
|
|
11
|
+
|
|
12
|
+
## [1.0.8] - 2025-12-12 (Hot-Patch)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Fixed SVGs not getting converted**: Seperated the logic to handle SVGs and Web Components/Icons.
|
|
17
|
+
|
|
18
|
+
|
|
5
19
|
## [1.0.7] - 2025-12-12
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dom-to-pptx
|
|
2
2
|
|
|
3
|
-
**The High-Fidelity HTML to PowerPoint Converter (v1.0.
|
|
3
|
+
**The High-Fidelity HTML to PowerPoint Converter (v1.0.8).**
|
|
4
4
|
|
|
5
5
|
Most HTML-to-PPTX libraries fail when faced with modern web design. They break on gradients, misalign text, ignore rounded corners, or simply take a screenshot (which isn't editable).
|
|
6
6
|
|
|
@@ -17752,6 +17752,7 @@
|
|
|
17752
17752
|
|
|
17753
17753
|
/**
|
|
17754
17754
|
* Determines if a given DOM node is primarily a text container.
|
|
17755
|
+
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
17755
17756
|
*/
|
|
17756
17757
|
function isTextContainer(node) {
|
|
17757
17758
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -17760,28 +17761,46 @@
|
|
|
17760
17761
|
const children = Array.from(node.children);
|
|
17761
17762
|
if (children.length === 0) return true;
|
|
17762
17763
|
|
|
17763
|
-
// Check if children are purely inline text formatting or visual shapes
|
|
17764
17764
|
const isSafeInline = (el) => {
|
|
17765
|
-
// 1. Reject Web Components /
|
|
17765
|
+
// 1. Reject Web Components / Custom Elements
|
|
17766
17766
|
if (el.tagName.includes('-')) return false;
|
|
17767
|
+
// 2. Reject Explicit Images/SVGs
|
|
17767
17768
|
if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
|
|
17768
17769
|
|
|
17770
|
+
// 3. Reject Class-based Icons (FontAwesome, Material, Bootstrap, etc.)
|
|
17771
|
+
// If an <i> or <span> has icon classes, it is a visual object, not text.
|
|
17772
|
+
if (el.tagName === 'I' || el.tagName === 'SPAN') {
|
|
17773
|
+
const cls = el.getAttribute('class') || '';
|
|
17774
|
+
if (
|
|
17775
|
+
cls.includes('fa-') ||
|
|
17776
|
+
cls.includes('fas') ||
|
|
17777
|
+
cls.includes('far') ||
|
|
17778
|
+
cls.includes('fab') ||
|
|
17779
|
+
cls.includes('material-icons') ||
|
|
17780
|
+
cls.includes('bi-') ||
|
|
17781
|
+
cls.includes('icon')
|
|
17782
|
+
) {
|
|
17783
|
+
return false;
|
|
17784
|
+
}
|
|
17785
|
+
}
|
|
17786
|
+
|
|
17769
17787
|
const style = window.getComputedStyle(el);
|
|
17770
17788
|
const display = style.display;
|
|
17771
17789
|
|
|
17772
|
-
//
|
|
17773
|
-
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
17790
|
+
// 4. Standard Inline Tag Check
|
|
17791
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
17792
|
+
el.tagName
|
|
17793
|
+
);
|
|
17774
17794
|
const isInlineDisplay = display.includes('inline');
|
|
17775
17795
|
|
|
17776
17796
|
if (!isInlineTag && !isInlineDisplay) return false;
|
|
17777
17797
|
|
|
17778
|
-
//
|
|
17779
|
-
//
|
|
17780
|
-
// If a child element has these, the parent is NOT a simple text container;
|
|
17781
|
-
// it is a layout container composed of styled blocks.
|
|
17798
|
+
// 5. Structural Styling Check
|
|
17799
|
+
// If a child has a background or border, it's a layout block, not a simple text span.
|
|
17782
17800
|
const bgColor = parseColor(style.backgroundColor);
|
|
17783
17801
|
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
17784
|
-
const hasBorder =
|
|
17802
|
+
const hasBorder =
|
|
17803
|
+
parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
17785
17804
|
|
|
17786
17805
|
if (hasVisibleBg || hasBorder) {
|
|
17787
17806
|
return false;
|
|
@@ -17808,6 +17827,69 @@
|
|
|
17808
17827
|
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
|
|
17809
17828
|
}
|
|
17810
17829
|
|
|
17830
|
+
function svgToPng(node) {
|
|
17831
|
+
return new Promise((resolve) => {
|
|
17832
|
+
const clone = node.cloneNode(true);
|
|
17833
|
+
const rect = node.getBoundingClientRect();
|
|
17834
|
+
const width = rect.width || 300;
|
|
17835
|
+
const height = rect.height || 150;
|
|
17836
|
+
|
|
17837
|
+
function inlineStyles(source, target) {
|
|
17838
|
+
const computed = window.getComputedStyle(source);
|
|
17839
|
+
const properties = [
|
|
17840
|
+
'fill',
|
|
17841
|
+
'stroke',
|
|
17842
|
+
'stroke-width',
|
|
17843
|
+
'stroke-linecap',
|
|
17844
|
+
'stroke-linejoin',
|
|
17845
|
+
'opacity',
|
|
17846
|
+
'font-family',
|
|
17847
|
+
'font-size',
|
|
17848
|
+
'font-weight',
|
|
17849
|
+
];
|
|
17850
|
+
|
|
17851
|
+
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
17852
|
+
else if (computed.fill) target.style.fill = computed.fill;
|
|
17853
|
+
|
|
17854
|
+
if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
|
|
17855
|
+
else if (computed.stroke) target.style.stroke = computed.stroke;
|
|
17856
|
+
|
|
17857
|
+
properties.forEach((prop) => {
|
|
17858
|
+
if (prop !== 'fill' && prop !== 'stroke') {
|
|
17859
|
+
const val = computed[prop];
|
|
17860
|
+
if (val && val !== 'auto') target.style[prop] = val;
|
|
17861
|
+
}
|
|
17862
|
+
});
|
|
17863
|
+
|
|
17864
|
+
for (let i = 0; i < source.children.length; i++) {
|
|
17865
|
+
if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
|
|
17866
|
+
}
|
|
17867
|
+
}
|
|
17868
|
+
|
|
17869
|
+
inlineStyles(node, clone);
|
|
17870
|
+
clone.setAttribute('width', width);
|
|
17871
|
+
clone.setAttribute('height', height);
|
|
17872
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
17873
|
+
|
|
17874
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
17875
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
17876
|
+
const img = new Image();
|
|
17877
|
+
img.crossOrigin = 'Anonymous';
|
|
17878
|
+
img.onload = () => {
|
|
17879
|
+
const canvas = document.createElement('canvas');
|
|
17880
|
+
const scale = 3;
|
|
17881
|
+
canvas.width = width * scale;
|
|
17882
|
+
canvas.height = height * scale;
|
|
17883
|
+
const ctx = canvas.getContext('2d');
|
|
17884
|
+
ctx.scale(scale, scale);
|
|
17885
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
17886
|
+
resolve(canvas.toDataURL('image/png'));
|
|
17887
|
+
};
|
|
17888
|
+
img.onerror = () => resolve(null);
|
|
17889
|
+
img.src = svgUrl;
|
|
17890
|
+
});
|
|
17891
|
+
}
|
|
17892
|
+
|
|
17811
17893
|
function getVisibleShadow(shadowStr, scale) {
|
|
17812
17894
|
if (!shadowStr || shadowStr === 'none') return null;
|
|
17813
17895
|
const shadows = shadowStr.split(/,(?![^()]*\))/);
|
|
@@ -17839,57 +17921,119 @@
|
|
|
17839
17921
|
return null;
|
|
17840
17922
|
}
|
|
17841
17923
|
|
|
17924
|
+
/**
|
|
17925
|
+
* Generates an SVG image for gradients, supporting degrees and keywords.
|
|
17926
|
+
*/
|
|
17842
17927
|
function generateGradientSVG(w, h, bgString, radius, border) {
|
|
17843
17928
|
try {
|
|
17844
17929
|
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
17845
17930
|
if (!match) return null;
|
|
17846
17931
|
const content = match[1];
|
|
17932
|
+
|
|
17933
|
+
// Split by comma, ignoring commas inside parentheses (e.g. rgba())
|
|
17847
17934
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
17935
|
+
if (parts.length < 2) return null;
|
|
17848
17936
|
|
|
17849
17937
|
let x1 = '0%',
|
|
17850
17938
|
y1 = '0%',
|
|
17851
17939
|
x2 = '0%',
|
|
17852
17940
|
y2 = '100%';
|
|
17853
|
-
let
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17941
|
+
let stopsStartIndex = 0;
|
|
17942
|
+
const firstPart = parts[0].toLowerCase();
|
|
17943
|
+
|
|
17944
|
+
// 1. Check for Keywords (to right, etc.)
|
|
17945
|
+
if (firstPart.startsWith('to ')) {
|
|
17946
|
+
stopsStartIndex = 1;
|
|
17947
|
+
const direction = firstPart.replace('to ', '').trim();
|
|
17948
|
+
switch (direction) {
|
|
17949
|
+
case 'top':
|
|
17950
|
+
y1 = '100%';
|
|
17951
|
+
y2 = '0%';
|
|
17952
|
+
break;
|
|
17953
|
+
case 'bottom':
|
|
17954
|
+
y1 = '0%';
|
|
17955
|
+
y2 = '100%';
|
|
17956
|
+
break;
|
|
17957
|
+
case 'left':
|
|
17958
|
+
x1 = '100%';
|
|
17959
|
+
x2 = '0%';
|
|
17960
|
+
break;
|
|
17961
|
+
case 'right':
|
|
17962
|
+
x2 = '100%';
|
|
17963
|
+
break;
|
|
17964
|
+
case 'top right':
|
|
17965
|
+
x1 = '0%';
|
|
17966
|
+
y1 = '100%';
|
|
17967
|
+
x2 = '100%';
|
|
17968
|
+
y2 = '0%';
|
|
17969
|
+
break;
|
|
17970
|
+
case 'top left':
|
|
17971
|
+
x1 = '100%';
|
|
17972
|
+
y1 = '100%';
|
|
17973
|
+
x2 = '0%';
|
|
17974
|
+
y2 = '0%';
|
|
17975
|
+
break;
|
|
17976
|
+
case 'bottom right':
|
|
17977
|
+
x2 = '100%';
|
|
17978
|
+
y2 = '100%';
|
|
17979
|
+
break;
|
|
17980
|
+
case 'bottom left':
|
|
17981
|
+
x1 = '100%';
|
|
17982
|
+
y2 = '100%';
|
|
17983
|
+
break;
|
|
17984
|
+
}
|
|
17985
|
+
}
|
|
17986
|
+
// 2. Check for Degrees (45deg, 90deg, etc.)
|
|
17987
|
+
else if (firstPart.match(/^-?[\d.]+(deg|rad|turn|grad)$/)) {
|
|
17988
|
+
stopsStartIndex = 1;
|
|
17989
|
+
const val = parseFloat(firstPart);
|
|
17990
|
+
// CSS 0deg is Top (North), 90deg is Right (East), 180deg is Bottom (South)
|
|
17991
|
+
// We convert this to SVG coordinates on a unit square (0-100%).
|
|
17992
|
+
// Formula: Map angle to perimeter coordinates.
|
|
17993
|
+
if (!isNaN(val)) {
|
|
17994
|
+
const deg = firstPart.includes('rad') ? val * (180 / Math.PI) : val;
|
|
17995
|
+
const cssRad = ((deg - 90) * Math.PI) / 180; // Correct CSS angle offset
|
|
17996
|
+
|
|
17997
|
+
// Calculate standard vector for rectangle center (50, 50)
|
|
17998
|
+
const scale = 50; // Distance from center to edge (approx)
|
|
17999
|
+
const cos = Math.cos(cssRad); // Y component (reversed in SVG)
|
|
18000
|
+
const sin = Math.sin(cssRad); // X component
|
|
18001
|
+
|
|
18002
|
+
// Invert Y for SVG coordinate system
|
|
18003
|
+
x1 = (50 - sin * scale).toFixed(1) + '%';
|
|
18004
|
+
y1 = (50 + cos * scale).toFixed(1) + '%';
|
|
18005
|
+
x2 = (50 + sin * scale).toFixed(1) + '%';
|
|
18006
|
+
y2 = (50 - cos * scale).toFixed(1) + '%';
|
|
18007
|
+
}
|
|
17872
18008
|
}
|
|
17873
18009
|
|
|
18010
|
+
// 3. Process Color Stops
|
|
17874
18011
|
let stopsXML = '';
|
|
17875
|
-
const stopParts = parts.slice(
|
|
18012
|
+
const stopParts = parts.slice(stopsStartIndex);
|
|
18013
|
+
|
|
17876
18014
|
stopParts.forEach((part, idx) => {
|
|
18015
|
+
// Parse "Color Position" (e.g., "red 50%")
|
|
18016
|
+
// Regex looks for optional space + number + unit at the end of the string
|
|
17877
18017
|
let color = part;
|
|
17878
18018
|
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
17879
|
-
|
|
18019
|
+
|
|
18020
|
+
const posMatch = part.match(/^(.*?)\s+(-?[\d.]+(?:%|px)?)$/);
|
|
17880
18021
|
if (posMatch) {
|
|
17881
18022
|
color = posMatch[1];
|
|
17882
18023
|
offset = posMatch[2];
|
|
17883
18024
|
}
|
|
18025
|
+
|
|
18026
|
+
// Handle RGBA/RGB for SVG compatibility
|
|
17884
18027
|
let opacity = 1;
|
|
17885
18028
|
if (color.includes('rgba')) {
|
|
17886
|
-
const
|
|
17887
|
-
if (
|
|
17888
|
-
opacity =
|
|
17889
|
-
color = `rgb(${
|
|
18029
|
+
const rgbaMatch = color.match(/[\d.]+/g);
|
|
18030
|
+
if (rgbaMatch && rgbaMatch.length >= 4) {
|
|
18031
|
+
opacity = rgbaMatch[3];
|
|
18032
|
+
color = `rgb(${rgbaMatch[0]},${rgbaMatch[1]},${rgbaMatch[2]})`;
|
|
17890
18033
|
}
|
|
17891
18034
|
}
|
|
17892
|
-
|
|
18035
|
+
|
|
18036
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color.trim()}" stop-opacity="${opacity}"/>`;
|
|
17893
18037
|
});
|
|
17894
18038
|
|
|
17895
18039
|
let strokeAttr = '';
|
|
@@ -17898,12 +18042,18 @@
|
|
|
17898
18042
|
}
|
|
17899
18043
|
|
|
17900
18044
|
const svg = `
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
|
|
18045
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
18046
|
+
<defs>
|
|
18047
|
+
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
|
18048
|
+
${stopsXML}
|
|
18049
|
+
</linearGradient>
|
|
18050
|
+
</defs>
|
|
18051
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
18052
|
+
</svg>`;
|
|
18053
|
+
|
|
17905
18054
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
17906
|
-
} catch {
|
|
18055
|
+
} catch (e) {
|
|
18056
|
+
console.warn('Gradient generation failed:', e);
|
|
17907
18057
|
return null;
|
|
17908
18058
|
}
|
|
17909
18059
|
}
|
|
@@ -18178,29 +18328,69 @@
|
|
|
18178
18328
|
* Optimized html2canvas wrapper
|
|
18179
18329
|
* Now strictly captures the node itself, not the root.
|
|
18180
18330
|
*/
|
|
18331
|
+
/**
|
|
18332
|
+
* Optimized html2canvas wrapper
|
|
18333
|
+
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
18334
|
+
*/
|
|
18181
18335
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
18182
18336
|
return new Promise((resolve) => {
|
|
18337
|
+
// 1. Assign a temp ID to locate the node inside the cloned document
|
|
18338
|
+
const originalId = node.id;
|
|
18339
|
+
const tempId = 'pptx-capture-' + Math.random().toString(36).substr(2, 9);
|
|
18340
|
+
node.id = tempId;
|
|
18341
|
+
|
|
18183
18342
|
const width = Math.max(Math.ceil(widthPx), 1);
|
|
18184
18343
|
const height = Math.max(Math.ceil(heightPx), 1);
|
|
18185
18344
|
const style = window.getComputedStyle(node);
|
|
18186
18345
|
|
|
18187
|
-
// Optimized: Capture ONLY the specific node
|
|
18188
18346
|
html2canvas(node, {
|
|
18189
18347
|
backgroundColor: null,
|
|
18190
18348
|
logging: false,
|
|
18191
|
-
scale:
|
|
18349
|
+
scale: 3, // Higher scale for sharper icons
|
|
18350
|
+
useCORS: true, // critical for external fonts/images
|
|
18351
|
+
onclone: (clonedDoc) => {
|
|
18352
|
+
const clonedNode = clonedDoc.getElementById(tempId);
|
|
18353
|
+
if (clonedNode) {
|
|
18354
|
+
// --- FIX: PREVENT ICON CLIPPING ---
|
|
18355
|
+
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
18356
|
+
clonedNode.style.overflow = 'visible';
|
|
18357
|
+
|
|
18358
|
+
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
18359
|
+
// (Applies to <i>, <span>, or standard icon classes)
|
|
18360
|
+
const tag = clonedNode.tagName;
|
|
18361
|
+
if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
|
|
18362
|
+
// Flex center helps align the glyph exactly in the middle of the box
|
|
18363
|
+
// preventing top/bottom cropping due to line-height mismatches.
|
|
18364
|
+
clonedNode.style.display = 'inline-flex';
|
|
18365
|
+
clonedNode.style.justifyContent = 'center';
|
|
18366
|
+
clonedNode.style.alignItems = 'center';
|
|
18367
|
+
|
|
18368
|
+
// Remove margins that might offset the capture
|
|
18369
|
+
clonedNode.style.margin = '0';
|
|
18370
|
+
|
|
18371
|
+
// Ensure the font fits
|
|
18372
|
+
clonedNode.style.lineHeight = '1';
|
|
18373
|
+
clonedNode.style.verticalAlign = 'middle';
|
|
18374
|
+
}
|
|
18375
|
+
}
|
|
18376
|
+
},
|
|
18192
18377
|
})
|
|
18193
18378
|
.then((canvas) => {
|
|
18379
|
+
// Restore the original ID
|
|
18380
|
+
if (originalId) node.id = originalId;
|
|
18381
|
+
else node.removeAttribute('id');
|
|
18382
|
+
|
|
18194
18383
|
const destCanvas = document.createElement('canvas');
|
|
18195
18384
|
destCanvas.width = width;
|
|
18196
18385
|
destCanvas.height = height;
|
|
18197
18386
|
const ctx = destCanvas.getContext('2d');
|
|
18198
18387
|
|
|
18199
|
-
// Draw
|
|
18200
|
-
//
|
|
18388
|
+
// Draw captured canvas.
|
|
18389
|
+
// We simply draw it to fill the box. Since we centered it in 'onclone',
|
|
18390
|
+
// the glyph should now be visible within the bounds.
|
|
18201
18391
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
18202
18392
|
|
|
18203
|
-
//
|
|
18393
|
+
// --- Border Radius Clipping (Existing Logic) ---
|
|
18204
18394
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
18205
18395
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
18206
18396
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -18239,12 +18429,62 @@
|
|
|
18239
18429
|
resolve(destCanvas.toDataURL('image/png'));
|
|
18240
18430
|
})
|
|
18241
18431
|
.catch((e) => {
|
|
18432
|
+
if (originalId) node.id = originalId;
|
|
18433
|
+
else node.removeAttribute('id');
|
|
18242
18434
|
console.warn('Canvas capture failed for node', node, e);
|
|
18243
18435
|
resolve(null);
|
|
18244
18436
|
});
|
|
18245
18437
|
});
|
|
18246
18438
|
}
|
|
18247
18439
|
|
|
18440
|
+
/**
|
|
18441
|
+
* Helper to identify elements that should be rendered as icons (Images).
|
|
18442
|
+
* Detects Custom Elements AND generic tags (<i>, <span>) with icon classes/pseudo-elements.
|
|
18443
|
+
*/
|
|
18444
|
+
function isIconElement(node) {
|
|
18445
|
+
// 1. Custom Elements (hyphenated tags) or Explicit Library Tags
|
|
18446
|
+
const tag = node.tagName.toUpperCase();
|
|
18447
|
+
if (
|
|
18448
|
+
tag.includes('-') ||
|
|
18449
|
+
[
|
|
18450
|
+
'MATERIAL-ICON',
|
|
18451
|
+
'ICONIFY-ICON',
|
|
18452
|
+
'REMIX-ICON',
|
|
18453
|
+
'ION-ICON',
|
|
18454
|
+
'EVA-ICON',
|
|
18455
|
+
'BOX-ICON',
|
|
18456
|
+
'FA-ICON',
|
|
18457
|
+
].includes(tag)
|
|
18458
|
+
) {
|
|
18459
|
+
return true;
|
|
18460
|
+
}
|
|
18461
|
+
|
|
18462
|
+
// 2. Class-based Icons (FontAwesome, Bootstrap, Material symbols) on <i> or <span>
|
|
18463
|
+
if (tag === 'I' || tag === 'SPAN') {
|
|
18464
|
+
const cls = node.getAttribute('class') || '';
|
|
18465
|
+
if (
|
|
18466
|
+
typeof cls === 'string' &&
|
|
18467
|
+
(cls.includes('fa-') ||
|
|
18468
|
+
cls.includes('fas') ||
|
|
18469
|
+
cls.includes('far') ||
|
|
18470
|
+
cls.includes('fab') ||
|
|
18471
|
+
cls.includes('bi-') ||
|
|
18472
|
+
cls.includes('material-icons') ||
|
|
18473
|
+
cls.includes('icon'))
|
|
18474
|
+
) {
|
|
18475
|
+
// Double-check: Must have pseudo-element content to be a CSS icon
|
|
18476
|
+
const before = window.getComputedStyle(node, '::before').content;
|
|
18477
|
+
const after = window.getComputedStyle(node, '::after').content;
|
|
18478
|
+
const hasContent = (c) => c && c !== 'none' && c !== 'normal' && c !== '""';
|
|
18479
|
+
|
|
18480
|
+
if (hasContent(before) || hasContent(after)) return true;
|
|
18481
|
+
}
|
|
18482
|
+
console.log('Icon element:', node, cls);
|
|
18483
|
+
}
|
|
18484
|
+
|
|
18485
|
+
return false;
|
|
18486
|
+
}
|
|
18487
|
+
|
|
18248
18488
|
/**
|
|
18249
18489
|
* Replaces createRenderItem.
|
|
18250
18490
|
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
@@ -18318,23 +18558,18 @@
|
|
|
18318
18558
|
|
|
18319
18559
|
const items = [];
|
|
18320
18560
|
|
|
18321
|
-
// --- ASYNC JOB:
|
|
18322
|
-
if (
|
|
18323
|
-
node.nodeName.toUpperCase() === 'SVG' ||
|
|
18324
|
-
node.tagName.includes('-') ||
|
|
18325
|
-
node.tagName === 'ION-ICON'
|
|
18326
|
-
) {
|
|
18561
|
+
// --- ASYNC JOB: SVG Tags ---
|
|
18562
|
+
if (node.nodeName.toUpperCase() === 'SVG') {
|
|
18327
18563
|
const item = {
|
|
18328
18564
|
type: 'image',
|
|
18329
18565
|
zIndex,
|
|
18330
18566
|
domOrder,
|
|
18331
|
-
options: { x, y, w, h, rotate: rotation
|
|
18567
|
+
options: { data: null, x, y, w, h, rotate: rotation },
|
|
18332
18568
|
};
|
|
18333
18569
|
|
|
18334
|
-
// Create Job
|
|
18335
18570
|
const job = async () => {
|
|
18336
|
-
const
|
|
18337
|
-
if (
|
|
18571
|
+
const processed = await svgToPng(node);
|
|
18572
|
+
if (processed) item.options.data = processed;
|
|
18338
18573
|
else item.skip = true;
|
|
18339
18574
|
};
|
|
18340
18575
|
|
|
@@ -18384,6 +18619,22 @@
|
|
|
18384
18619
|
return { items: [item], job, stopRecursion: true };
|
|
18385
18620
|
}
|
|
18386
18621
|
|
|
18622
|
+
// --- ASYNC JOB: Icons and Other Elements ---
|
|
18623
|
+
if (isIconElement(node)) {
|
|
18624
|
+
const item = {
|
|
18625
|
+
type: 'image',
|
|
18626
|
+
zIndex,
|
|
18627
|
+
domOrder,
|
|
18628
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
18629
|
+
};
|
|
18630
|
+
const job = async () => {
|
|
18631
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
18632
|
+
if (pngData) item.options.data = pngData;
|
|
18633
|
+
else item.skip = true;
|
|
18634
|
+
};
|
|
18635
|
+
return { items: [item], job, stopRecursion: true };
|
|
18636
|
+
}
|
|
18637
|
+
|
|
18387
18638
|
// Radii logic
|
|
18388
18639
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
18389
18640
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|