@microlee666/dom-to-pptx 1.1.4 → 1.1.6
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/.claude/settings.local.json +12 -0
- package/Readme.md +28 -0
- package/SUPPORTED.md +5 -0
- package/USAGE_CN.md +26 -0
- package/cli/dom-to-pptx.bundle.js +977 -36
- package/cli/html2pptx.js +19 -6
- package/cli/presentation.pptx +0 -0
- package/dist/dom-to-pptx.bundle.js +769 -14
- package/dist/dom-to-pptx.cjs +767 -13
- package/dist/dom-to-pptx.cjs.map +1 -1
- package/dist/dom-to-pptx.mjs +767 -13
- package/dist/dom-to-pptx.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/patch-bundle.js +66 -0
- package/src/index.js +530 -10
- package/src/utils.js +240 -4
package/src/index.js
CHANGED
|
@@ -25,13 +25,68 @@ import {
|
|
|
25
25
|
getUsedFontFamilies,
|
|
26
26
|
getAutoDetectedFonts,
|
|
27
27
|
extractTableData,
|
|
28
|
-
computeTableCellFill
|
|
28
|
+
computeTableCellFill,
|
|
29
|
+
buildChartRegistry
|
|
29
30
|
} from './utils.js';
|
|
30
31
|
import { getProcessedImage } from './image-processor.js';
|
|
31
32
|
|
|
32
33
|
const PPI = 96;
|
|
33
34
|
const PX_TO_INCH = 1 / PPI;
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Fix Chinese font attributes (pitchFamily and charset) in PPTX XML.
|
|
38
|
+
* PptxGenJS sometimes generates incorrect charset="0" for East Asian fonts.
|
|
39
|
+
* @param {Blob} blob - The PPTX blob
|
|
40
|
+
* @returns {Promise<Blob>} - Fixed PPTX blob
|
|
41
|
+
*/
|
|
42
|
+
async function fixChineseFontAttributes(blob) {
|
|
43
|
+
const zip = await JSZip.loadAsync(blob);
|
|
44
|
+
const cjkFonts = ['Heiti TC', 'Heiti SC', 'PingFang SC', 'Microsoft YaHei', 'SimHei', 'SimSun'];
|
|
45
|
+
|
|
46
|
+
// Process all slide XML files
|
|
47
|
+
const slideFiles = Object.keys(zip.files).filter(name =>
|
|
48
|
+
name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const slidePath of slideFiles) {
|
|
52
|
+
let xml = await zip.file(slidePath).async('text');
|
|
53
|
+
let modified = false;
|
|
54
|
+
|
|
55
|
+
// Fix <a:ea> and <a:cs> tags for CJK fonts
|
|
56
|
+
// Replace tags that have charset="0" (incorrect for CJK) with proper values
|
|
57
|
+
for (const fontName of cjkFonts) {
|
|
58
|
+
// Fix <a:ea> - East Asian font
|
|
59
|
+
const eaPattern = new RegExp(
|
|
60
|
+
`<a:ea typeface="${fontName}"[^>]*>`,
|
|
61
|
+
'g'
|
|
62
|
+
);
|
|
63
|
+
xml = xml.replace(eaPattern, `<a:ea typeface="${fontName}" pitchFamily="34" charset="-122"/>`);
|
|
64
|
+
|
|
65
|
+
// Fix <a:cs> - Complex Script font
|
|
66
|
+
const csPattern = new RegExp(
|
|
67
|
+
`<a:cs typeface="${fontName}"[^>]*>`,
|
|
68
|
+
'g'
|
|
69
|
+
);
|
|
70
|
+
xml = xml.replace(csPattern, `<a:cs typeface="${fontName}" pitchFamily="34" charset="-120"/>`);
|
|
71
|
+
|
|
72
|
+
// Fix <a:latin> - Latin font (if it's a CJK font used as Latin)
|
|
73
|
+
const latinPattern = new RegExp(
|
|
74
|
+
`<a:latin typeface="${fontName}"[^>]*>`,
|
|
75
|
+
'g'
|
|
76
|
+
);
|
|
77
|
+
xml = xml.replace(latinPattern, `<a:latin typeface="${fontName}" pitchFamily="34" charset="0"/>`);
|
|
78
|
+
}
|
|
79
|
+
modified = true;
|
|
80
|
+
|
|
81
|
+
if (modified) {
|
|
82
|
+
zip.file(slidePath, xml);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Generate new blob
|
|
87
|
+
return await zip.generateAsync({ type: 'blob' });
|
|
88
|
+
}
|
|
89
|
+
|
|
35
90
|
/**
|
|
36
91
|
* Main export function.
|
|
37
92
|
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
@@ -56,6 +111,7 @@ export async function exportToPptx(target, options = {}) {
|
|
|
56
111
|
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
57
112
|
const pptx = new PptxConstructor();
|
|
58
113
|
pptx.layout = 'LAYOUT_16x9';
|
|
114
|
+
pptx.language = 'zh-CN';
|
|
59
115
|
|
|
60
116
|
const elements = Array.isArray(target) ? target : [target];
|
|
61
117
|
|
|
@@ -130,6 +186,9 @@ export async function exportToPptx(target, options = {}) {
|
|
|
130
186
|
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
131
187
|
}
|
|
132
188
|
|
|
189
|
+
// Fix Chinese font attributes (pitchFamily and charset) in all slide XMLs
|
|
190
|
+
finalBlob = await fixChineseFontAttributes(finalBlob);
|
|
191
|
+
|
|
133
192
|
// 4. Output Handling
|
|
134
193
|
// If skipDownload is NOT true, proceed with browser download
|
|
135
194
|
if (!options.skipDownload) {
|
|
@@ -171,6 +230,9 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
171
230
|
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
172
231
|
};
|
|
173
232
|
|
|
233
|
+
const chartRegistry = buildChartRegistry(root, globalOptions.chartConfigs);
|
|
234
|
+
const slideOptions = { ...globalOptions, chartRegistry };
|
|
235
|
+
|
|
174
236
|
const renderQueue = [];
|
|
175
237
|
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
176
238
|
let domOrderCounter = 0;
|
|
@@ -206,7 +268,7 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
206
268
|
pptx,
|
|
207
269
|
currentZ,
|
|
208
270
|
nodeStyle,
|
|
209
|
-
|
|
271
|
+
slideOptions
|
|
210
272
|
);
|
|
211
273
|
|
|
212
274
|
if (result) {
|
|
@@ -252,6 +314,10 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
252
314
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
253
315
|
if (item.type === 'image') slide.addImage(item.options);
|
|
254
316
|
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
317
|
+
if (item.type === 'chart') {
|
|
318
|
+
const chartType = PptxGenJS.ChartType?.[item.chartType] ?? item.chartType;
|
|
319
|
+
slide.addChart(chartType, item.chartData, item.options);
|
|
320
|
+
}
|
|
255
321
|
if (item.type === 'table') {
|
|
256
322
|
slide.addTable(item.tableData.rows, {
|
|
257
323
|
x: item.options.x,
|
|
@@ -452,13 +518,23 @@ function prepareRenderItem(
|
|
|
452
518
|
range.detach();
|
|
453
519
|
|
|
454
520
|
const style = window.getComputedStyle(parent);
|
|
455
|
-
|
|
456
|
-
|
|
521
|
+
// Use parent element's rect for better width calculation
|
|
522
|
+
// This is especially important for flex children where text node rect may be too narrow
|
|
523
|
+
const parentRect = parent.getBoundingClientRect();
|
|
524
|
+
const useParentRect = parentRect.width > rect.width;
|
|
525
|
+
let widthPx = useParentRect ? parentRect.width : rect.width;
|
|
526
|
+
const heightPx = useParentRect ? parentRect.height : rect.height;
|
|
527
|
+
|
|
528
|
+
// Add extra width buffer to prevent text wrapping in PPTX due to font rendering differences
|
|
529
|
+
// Chinese characters and certain fonts need more space in PPTX than in browser
|
|
530
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
457
531
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
458
532
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
459
533
|
|
|
460
|
-
|
|
461
|
-
const
|
|
534
|
+
// Use parent rect's position if we're using parent dimensions
|
|
535
|
+
const sourceRect = useParentRect ? parentRect : rect;
|
|
536
|
+
const x = config.offX + (sourceRect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
537
|
+
const y = config.offY + (sourceRect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
462
538
|
|
|
463
539
|
return {
|
|
464
540
|
items: [
|
|
@@ -497,7 +573,7 @@ function prepareRenderItem(
|
|
|
497
573
|
const elementOpacity = parseFloat(style.opacity);
|
|
498
574
|
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
499
575
|
|
|
500
|
-
|
|
576
|
+
let widthPx = node.offsetWidth || rect.width;
|
|
501
577
|
const heightPx = node.offsetHeight || rect.height;
|
|
502
578
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
503
579
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
@@ -511,8 +587,41 @@ function prepareRenderItem(
|
|
|
511
587
|
|
|
512
588
|
const items = [];
|
|
513
589
|
|
|
514
|
-
|
|
515
|
-
|
|
590
|
+
const chartConfig = globalOptions.chartRegistry?.get(node);
|
|
591
|
+
if (chartConfig) {
|
|
592
|
+
const chartOptions = {
|
|
593
|
+
...chartConfig.chartOptions,
|
|
594
|
+
x,
|
|
595
|
+
y,
|
|
596
|
+
w: unrotatedW,
|
|
597
|
+
h: unrotatedH,
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
items: [
|
|
602
|
+
{
|
|
603
|
+
type: 'chart',
|
|
604
|
+
zIndex: effectiveZIndex,
|
|
605
|
+
domOrder,
|
|
606
|
+
chartType: chartConfig.chartType,
|
|
607
|
+
chartData: chartConfig.chartData,
|
|
608
|
+
options: chartOptions,
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
stopRecursion: true,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// --- Handle both native TABLE and div-based table structures ---
|
|
616
|
+
const isTableLike = node.tagName === 'TABLE' || detectTableLikeStructure(node);
|
|
617
|
+
if (isTableLike) {
|
|
618
|
+
let tableData;
|
|
619
|
+
if (node.tagName === 'TABLE') {
|
|
620
|
+
tableData = extractTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
621
|
+
} else {
|
|
622
|
+
// Extract data from div-based table structure
|
|
623
|
+
tableData = extractDivTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
624
|
+
}
|
|
516
625
|
const cellBgItems = [];
|
|
517
626
|
const renderCellBg = globalOptions.tableConfig?.renderCellBackgrounds !== false;
|
|
518
627
|
|
|
@@ -856,6 +965,36 @@ function prepareRenderItem(
|
|
|
856
965
|
return { items: [item], job, stopRecursion: true };
|
|
857
966
|
}
|
|
858
967
|
|
|
968
|
+
// --- Handle vertical stat cards (like .mini-stat with number + label) ---
|
|
969
|
+
if (isVerticalStatCard(node)) {
|
|
970
|
+
// Capture the entire stat card as an image to preserve vertical layout
|
|
971
|
+
const item = {
|
|
972
|
+
type: 'image',
|
|
973
|
+
zIndex,
|
|
974
|
+
domOrder,
|
|
975
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
976
|
+
};
|
|
977
|
+
const job = async () => {
|
|
978
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
979
|
+
if (pngData) item.options.data = pngData;
|
|
980
|
+
else item.skip = true;
|
|
981
|
+
};
|
|
982
|
+
return { items: [item], job, stopRecursion: true };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// --- Handle absolutely positioned children that overflow parent ---
|
|
986
|
+
// Check if this element has absolutely positioned children that extend outside
|
|
987
|
+
const overflowingChildren = detectOverflowingChildren(node);
|
|
988
|
+
if (overflowingChildren.length > 0) {
|
|
989
|
+
// Process this node normally, but also capture overflowing children separately
|
|
990
|
+
const baseResult = processOverflowingContent(
|
|
991
|
+
node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions
|
|
992
|
+
);
|
|
993
|
+
if (baseResult) {
|
|
994
|
+
return baseResult;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
859
998
|
// Radii logic
|
|
860
999
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
861
1000
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
@@ -958,6 +1097,27 @@ function prepareRenderItem(
|
|
|
958
1097
|
let textPayload = null;
|
|
959
1098
|
const isText = isTextContainer(node);
|
|
960
1099
|
|
|
1100
|
+
// Add extra width buffer for inline text elements to prevent wrapping in PPTX
|
|
1101
|
+
// This is especially needed for Chinese text and certain fonts
|
|
1102
|
+
// Only apply to elements that are direct children of flex containers with space distribution
|
|
1103
|
+
// and don't have a parent with visible background (to avoid overflowing card boundaries)
|
|
1104
|
+
if (isText && !bgColorObj.hex && !hasBorder && !hasShadow) {
|
|
1105
|
+
const parentEl = node.parentElement;
|
|
1106
|
+
if (parentEl) {
|
|
1107
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
1108
|
+
const parentDisplay = parentStyle.display || '';
|
|
1109
|
+
const parentJustify = parentStyle.justifyContent || '';
|
|
1110
|
+
const isFlexWithSpaceDist = (parentDisplay.includes('flex') || parentDisplay.includes('grid')) &&
|
|
1111
|
+
(parentJustify.includes('space-between') || parentJustify.includes('space-around') || parentJustify.includes('space-evenly'));
|
|
1112
|
+
|
|
1113
|
+
if (isFlexWithSpaceDist) {
|
|
1114
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
1115
|
+
// Recalculate w with the new width
|
|
1116
|
+
w = widthPx * PX_TO_INCH * config.scale;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
961
1121
|
if (isText) {
|
|
962
1122
|
const textParts = [];
|
|
963
1123
|
let trimNextLeading = false;
|
|
@@ -1014,8 +1174,91 @@ function prepareRenderItem(
|
|
|
1014
1174
|
let align = style.textAlign || 'left';
|
|
1015
1175
|
if (align === 'start') align = 'left';
|
|
1016
1176
|
if (align === 'end') align = 'right';
|
|
1177
|
+
|
|
1178
|
+
// Fix: If this element is a flex/grid child and has no explicit text-align,
|
|
1179
|
+
// force left alignment to match HTML default behavior
|
|
1180
|
+
if (node.parentElement && (!style.textAlign || style.textAlign === 'start')) {
|
|
1181
|
+
const parentStyle = window.getComputedStyle(node.parentElement);
|
|
1182
|
+
if (parentStyle.display.includes('flex') || parentStyle.display.includes('grid')) {
|
|
1183
|
+
align = 'left';
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Detect badge/pill buttons (high border-radius + short text) and auto-center them
|
|
1188
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
1189
|
+
const height = parseFloat(style.height) || node.offsetHeight;
|
|
1190
|
+
const textContent = node.textContent.trim();
|
|
1191
|
+
const className = (node.className || '').toLowerCase();
|
|
1192
|
+
|
|
1193
|
+
// Real badges/pills typically have visible styling (background, border, or large border-radius)
|
|
1194
|
+
const hasVisibleBackground = (bgColorObj.hex && bgColorObj.opacity > 0.1) || hasGradient;
|
|
1195
|
+
const hasVisibleBorder = borderWidth > 0;
|
|
1196
|
+
const hasLargeBorderRadius = borderRadius >= height / 2;
|
|
1197
|
+
const hasBadgeClass = className.includes('badge') || className.includes('pill') || className.includes('tag');
|
|
1198
|
+
|
|
1199
|
+
// Check if it's a small tag/label with rounded corners and short text
|
|
1200
|
+
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
1201
|
+
const paddingRight = parseFloat(style.paddingRight) || 0;
|
|
1202
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
1203
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
1204
|
+
const hasSmallPadding = Math.max(paddingLeft, paddingRight, paddingTop, paddingBottom) <= 12;
|
|
1205
|
+
const hasEvenPadding = Math.abs(paddingTop - paddingBottom) <= 4 && Math.abs(paddingLeft - paddingRight) <= 8;
|
|
1206
|
+
const hasRoundedCorners = borderRadius >= 3;
|
|
1207
|
+
const isInlineBlock = style.display === 'inline-block' || style.display === 'inline-flex';
|
|
1208
|
+
|
|
1209
|
+
// Small tag detection: inline-block with background, rounded corners, small even padding, and short text
|
|
1210
|
+
const isSmallTag =
|
|
1211
|
+
isInlineBlock &&
|
|
1212
|
+
hasVisibleBackground &&
|
|
1213
|
+
hasRoundedCorners &&
|
|
1214
|
+
hasSmallPadding &&
|
|
1215
|
+
hasEvenPadding &&
|
|
1216
|
+
textContent.length <= 10;
|
|
1217
|
+
|
|
1218
|
+
// Only consider it a badge if it has visual styling AND short text
|
|
1219
|
+
const isLikelyBadge =
|
|
1220
|
+
((hasLargeBorderRadius || hasBadgeClass) &&
|
|
1221
|
+
textContent.length <= 10 &&
|
|
1222
|
+
(hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius)) ||
|
|
1223
|
+
isSmallTag;
|
|
1224
|
+
|
|
1225
|
+
if (isLikelyBadge) {
|
|
1226
|
+
align = 'center';
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1017
1229
|
let valign = 'top';
|
|
1018
|
-
|
|
1230
|
+
|
|
1231
|
+
// For flex items, valign should be determined by PARENT's align-items, not self
|
|
1232
|
+
// Self's align-items controls how children are aligned, not self's position in parent
|
|
1233
|
+
const parentEl = node.parentElement;
|
|
1234
|
+
if (parentEl) {
|
|
1235
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
1236
|
+
if (parentStyle.display.includes('flex')) {
|
|
1237
|
+
// Parent's align-items controls this element's cross-axis alignment
|
|
1238
|
+
if (parentStyle.alignItems === 'center') {
|
|
1239
|
+
valign = 'middle';
|
|
1240
|
+
} else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
|
|
1241
|
+
valign = 'bottom';
|
|
1242
|
+
}
|
|
1243
|
+
// Default (stretch, flex-start) keeps valign = 'top'
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// If element is not a flex item (no flex parent), then its own align-items
|
|
1248
|
+
// might indicate self-centering intent for single-element containers
|
|
1249
|
+
if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
|
|
1250
|
+
valign = 'middle';
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Auto-center vertically for likely badges
|
|
1254
|
+
if (isLikelyBadge && valign !== 'middle') {
|
|
1255
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
1256
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
1257
|
+
// If padding is relatively even, assume vertical centering is intended
|
|
1258
|
+
if (Math.abs(paddingTop - paddingBottom) <= 4) {
|
|
1259
|
+
valign = 'middle';
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1019
1262
|
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
1020
1263
|
|
|
1021
1264
|
const pt = parseFloat(style.paddingTop) || 0;
|
|
@@ -1374,3 +1617,280 @@ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder)
|
|
|
1374
1617
|
|
|
1375
1618
|
return items;
|
|
1376
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Detect absolutely positioned children that overflow their parent
|
|
1623
|
+
* This handles cases like chart value labels positioned above bars
|
|
1624
|
+
*/
|
|
1625
|
+
function detectOverflowingChildren(node) {
|
|
1626
|
+
const overflowing = [];
|
|
1627
|
+
const parentRect = node.getBoundingClientRect();
|
|
1628
|
+
|
|
1629
|
+
// Only check for elements with specific class patterns that suggest chart values
|
|
1630
|
+
const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
|
|
1631
|
+
|
|
1632
|
+
children.forEach(child => {
|
|
1633
|
+
const childStyle = window.getComputedStyle(child);
|
|
1634
|
+
const childRect = child.getBoundingClientRect();
|
|
1635
|
+
|
|
1636
|
+
// Check if absolutely positioned and outside parent bounds
|
|
1637
|
+
if (childStyle.position === 'absolute') {
|
|
1638
|
+
const isOutside =
|
|
1639
|
+
childRect.bottom < parentRect.top ||
|
|
1640
|
+
childRect.top > parentRect.bottom ||
|
|
1641
|
+
childRect.right < parentRect.left ||
|
|
1642
|
+
childRect.left > parentRect.right;
|
|
1643
|
+
|
|
1644
|
+
if (isOutside || childRect.top < parentRect.top) {
|
|
1645
|
+
overflowing.push(child);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
return overflowing;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Process content with overflowing children
|
|
1655
|
+
* Captures the entire visual area including overflowing elements
|
|
1656
|
+
*/
|
|
1657
|
+
function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
|
|
1658
|
+
// Calculate bounding box that includes all overflowing children
|
|
1659
|
+
const rect = node.getBoundingClientRect();
|
|
1660
|
+
let minX = rect.left;
|
|
1661
|
+
let minY = rect.top;
|
|
1662
|
+
let maxX = rect.right;
|
|
1663
|
+
let maxY = rect.bottom;
|
|
1664
|
+
|
|
1665
|
+
overflowingChildren.forEach(child => {
|
|
1666
|
+
const childRect = child.getBoundingClientRect();
|
|
1667
|
+
minX = Math.min(minX, childRect.left);
|
|
1668
|
+
minY = Math.min(minY, childRect.top);
|
|
1669
|
+
maxX = Math.max(maxX, childRect.right);
|
|
1670
|
+
maxY = Math.max(maxY, childRect.bottom);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
const totalWidth = maxX - minX;
|
|
1674
|
+
const totalHeight = maxY - minY;
|
|
1675
|
+
|
|
1676
|
+
// Use html2canvas to capture the entire area
|
|
1677
|
+
const item = {
|
|
1678
|
+
type: 'image',
|
|
1679
|
+
zIndex,
|
|
1680
|
+
domOrder,
|
|
1681
|
+
options: {
|
|
1682
|
+
x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
|
|
1683
|
+
y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
|
|
1684
|
+
w: totalWidth * PX_TO_INCH * config.scale,
|
|
1685
|
+
h: totalHeight * PX_TO_INCH * config.scale,
|
|
1686
|
+
data: null
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
const job = async () => {
|
|
1691
|
+
try {
|
|
1692
|
+
// Temporarily adjust node position for capture
|
|
1693
|
+
const originalPosition = node.style.position;
|
|
1694
|
+
const originalTransform = node.style.transform;
|
|
1695
|
+
|
|
1696
|
+
node.style.position = 'relative';
|
|
1697
|
+
node.style.transform = 'none';
|
|
1698
|
+
|
|
1699
|
+
const canvas = await html2canvas(node, {
|
|
1700
|
+
backgroundColor: null,
|
|
1701
|
+
logging: false,
|
|
1702
|
+
scale: 2,
|
|
1703
|
+
useCORS: true,
|
|
1704
|
+
x: minX - rect.left,
|
|
1705
|
+
y: minY - rect.top,
|
|
1706
|
+
width: totalWidth,
|
|
1707
|
+
height: totalHeight
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
// Restore original styles
|
|
1711
|
+
node.style.position = originalPosition;
|
|
1712
|
+
node.style.transform = originalTransform;
|
|
1713
|
+
|
|
1714
|
+
item.options.data = canvas.toDataURL('image/png');
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
console.warn('Failed to capture overflowing content:', e);
|
|
1717
|
+
item.skip = true;
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
return { items: [item], job, stopRecursion: true };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* Detect vertical stat cards (like .mini-stat with number above label)
|
|
1726
|
+
* These have block-level children that should stack vertically
|
|
1727
|
+
*/
|
|
1728
|
+
function isVerticalStatCard(node) {
|
|
1729
|
+
const className = (node.className || '').toLowerCase();
|
|
1730
|
+
|
|
1731
|
+
// Check for stat-like class names
|
|
1732
|
+
const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
|
|
1733
|
+
const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
|
|
1734
|
+
|
|
1735
|
+
if (!hasStatClass) return false;
|
|
1736
|
+
|
|
1737
|
+
const children = Array.from(node.children);
|
|
1738
|
+
if (children.length !== 2) return false;
|
|
1739
|
+
|
|
1740
|
+
// Check if children are likely number + label pair
|
|
1741
|
+
const child1 = children[0];
|
|
1742
|
+
const child2 = children[1];
|
|
1743
|
+
|
|
1744
|
+
const style1 = window.getComputedStyle(child1);
|
|
1745
|
+
const style2 = window.getComputedStyle(child2);
|
|
1746
|
+
|
|
1747
|
+
// Both should be block elements (or have block-like display)
|
|
1748
|
+
const isBlock1 = style1.display === 'block' || style1.display === 'flex';
|
|
1749
|
+
const isBlock2 = style2.display === 'block' || style2.display === 'flex';
|
|
1750
|
+
|
|
1751
|
+
if (!isBlock1 || !isBlock2) return false;
|
|
1752
|
+
|
|
1753
|
+
// First child should have larger font (the number)
|
|
1754
|
+
const fontSize1 = parseFloat(style1.fontSize) || 0;
|
|
1755
|
+
const fontSize2 = parseFloat(style2.fontSize) || 0;
|
|
1756
|
+
|
|
1757
|
+
// Number should be larger than label, or at least bold
|
|
1758
|
+
const isBold1 = parseInt(style1.fontWeight) >= 600;
|
|
1759
|
+
|
|
1760
|
+
return fontSize1 >= fontSize2 || isBold1;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Detect if a div structure looks like a table
|
|
1765
|
+
* Checks for table-like class names and structure patterns
|
|
1766
|
+
*/
|
|
1767
|
+
function detectTableLikeStructure(node) {
|
|
1768
|
+
const className = (node.className || '').toLowerCase();
|
|
1769
|
+
|
|
1770
|
+
// Check for table-related class names
|
|
1771
|
+
const tableIndicators = ['table', 'data-table', 'grid', 'list'];
|
|
1772
|
+
const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
|
|
1773
|
+
|
|
1774
|
+
if (!hasTableClass) return false;
|
|
1775
|
+
|
|
1776
|
+
// Check structure: should have header row and data rows
|
|
1777
|
+
const children = Array.from(node.children);
|
|
1778
|
+
|
|
1779
|
+
// Look for header-like element
|
|
1780
|
+
const hasHeader = children.some(child =>
|
|
1781
|
+
(child.className || '').toLowerCase().includes('header') ||
|
|
1782
|
+
child.tagName === 'THEAD'
|
|
1783
|
+
);
|
|
1784
|
+
|
|
1785
|
+
// Look for row-like elements
|
|
1786
|
+
const hasRows = children.some(child =>
|
|
1787
|
+
(child.className || '').toLowerCase().includes('row') ||
|
|
1788
|
+
child.tagName === 'TR' ||
|
|
1789
|
+
child.tagName === 'TBODY'
|
|
1790
|
+
);
|
|
1791
|
+
|
|
1792
|
+
// Check for cell-like structure in children
|
|
1793
|
+
const hasCellStructure = children.some(child => {
|
|
1794
|
+
const childChildren = Array.from(child.children);
|
|
1795
|
+
return childChildren.some(grandchild =>
|
|
1796
|
+
(grandchild.className || '').toLowerCase().includes('cell') ||
|
|
1797
|
+
grandchild.className.toLowerCase().includes('col') ||
|
|
1798
|
+
grandchild.className.toLowerCase().includes('td')
|
|
1799
|
+
);
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
return (hasHeader || hasRows) && hasCellStructure;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Extract table data from div-based table structure
|
|
1807
|
+
* Similar to extractTableData but for div grids
|
|
1808
|
+
*/
|
|
1809
|
+
function extractDivTableData(node, scale, options = {}) {
|
|
1810
|
+
const rows = [];
|
|
1811
|
+
const colWidths = [];
|
|
1812
|
+
const root = options.root || null;
|
|
1813
|
+
|
|
1814
|
+
// Find header row
|
|
1815
|
+
const headerRow = node.querySelector('.table-header, [class*="header"]');
|
|
1816
|
+
if (headerRow) {
|
|
1817
|
+
const headerCells = Array.from(headerRow.children);
|
|
1818
|
+
headerCells.forEach((cell, index) => {
|
|
1819
|
+
const rect = cell.getBoundingClientRect();
|
|
1820
|
+
const wIn = rect.width * (1 / 96) * scale;
|
|
1821
|
+
colWidths[index] = wIn;
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
// Process header as first row
|
|
1825
|
+
const headerData = headerCells.map(cell => {
|
|
1826
|
+
const style = window.getComputedStyle(cell);
|
|
1827
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
1828
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
1829
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
1830
|
+
|
|
1831
|
+
return {
|
|
1832
|
+
text: cellText,
|
|
1833
|
+
options: {
|
|
1834
|
+
...textStyle,
|
|
1835
|
+
fill: fill || { color: '16A085', transparency: 80 },
|
|
1836
|
+
bold: true,
|
|
1837
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
1838
|
+
border: { type: 'none' }
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
});
|
|
1842
|
+
rows.push(headerData);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Find data rows
|
|
1846
|
+
const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
|
|
1847
|
+
dataRows.forEach(row => {
|
|
1848
|
+
const cells = Array.from(row.children);
|
|
1849
|
+
const rowData = cells.map(cell => {
|
|
1850
|
+
const style = window.getComputedStyle(cell);
|
|
1851
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
1852
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
1853
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
1854
|
+
|
|
1855
|
+
// Detect trend colors
|
|
1856
|
+
const className = (cell.className || '').toLowerCase();
|
|
1857
|
+
let textColor = textStyle.color;
|
|
1858
|
+
if (className.includes('up') || className.includes('positive')) {
|
|
1859
|
+
textColor = '16A085';
|
|
1860
|
+
} else if (className.includes('down') || className.includes('negative')) {
|
|
1861
|
+
textColor = 'E74C3C';
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
return {
|
|
1865
|
+
text: cellText,
|
|
1866
|
+
options: {
|
|
1867
|
+
...textStyle,
|
|
1868
|
+
color: textColor,
|
|
1869
|
+
fill: fill,
|
|
1870
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
1871
|
+
border: {
|
|
1872
|
+
type: 'none',
|
|
1873
|
+
bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
if (rowData.length > 0) {
|
|
1880
|
+
rows.push(rowData);
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
// Ensure all rows have same column count
|
|
1885
|
+
const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
|
|
1886
|
+
rows.forEach(row => {
|
|
1887
|
+
while (row.length < maxCols) {
|
|
1888
|
+
row.push({ text: '', options: {} });
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
while (colWidths.length < maxCols) {
|
|
1892
|
+
colWidths.push(colWidths[colWidths.length - 1] || 1);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
return { rows, colWidths };
|
|
1896
|
+
}
|