@microlee666/dom-to-pptx 1.1.4 → 1.1.5
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 +11 -0
- package/Readme.md +28 -0
- package/SUPPORTED.md +5 -0
- package/USAGE_CN.md +26 -0
- package/cli/dom-to-pptx.bundle.js +957 -36
- package/cli/html2pptx.js +19 -6
- package/cli/presentation.pptx +0 -0
- package/dist/dom-to-pptx.bundle.js +749 -14
- package/dist/dom-to-pptx.cjs +747 -13
- package/dist/dom-to-pptx.cjs.map +1 -1
- package/dist/dom-to-pptx.mjs +747 -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 +510 -10
- package/src/utils.js +240 -4
|
@@ -7713,7 +7713,7 @@
|
|
|
7713
7713
|
* @param {number} b - blue value
|
|
7714
7714
|
* @returns {string} XML string
|
|
7715
7715
|
*/
|
|
7716
|
-
function rgbToHex(r, g, b) {
|
|
7716
|
+
function rgbToHex$1(r, g, b) {
|
|
7717
7717
|
return (componentToHex(r) + componentToHex(g) + componentToHex(b)).toUpperCase();
|
|
7718
7718
|
}
|
|
7719
7719
|
/** TODO: FUTURE: TODO-4.0:
|
|
@@ -8460,8 +8460,8 @@
|
|
|
8460
8460
|
bold: !!(window.getComputedStyle(cell).getPropertyValue('font-weight') === 'bold' ||
|
|
8461
8461
|
Number(window.getComputedStyle(cell).getPropertyValue('font-weight')) >= 500),
|
|
8462
8462
|
border: null,
|
|
8463
|
-
color: rgbToHex(Number(arrRGB1[0]), Number(arrRGB1[1]), Number(arrRGB1[2])),
|
|
8464
|
-
fill: { color: rgbToHex(Number(arrRGB2[0]), Number(arrRGB2[1]), Number(arrRGB2[2])) },
|
|
8463
|
+
color: rgbToHex$1(Number(arrRGB1[0]), Number(arrRGB1[1]), Number(arrRGB1[2])),
|
|
8464
|
+
fill: { color: rgbToHex$1(Number(arrRGB2[0]), Number(arrRGB2[1]), Number(arrRGB2[2])) },
|
|
8465
8465
|
fontFace: (window.getComputedStyle(cell).getPropertyValue('font-family') || '').split(',')[0].replace(/"/g, '').replace('inherit', '').replace('initial', '') ||
|
|
8466
8466
|
null,
|
|
8467
8467
|
fontSize: Number(window.getComputedStyle(cell).getPropertyValue('font-size').replace(/[a-z]/gi, '')),
|
|
@@ -8508,7 +8508,7 @@
|
|
|
8508
8508
|
.replace('rgb(', '')
|
|
8509
8509
|
.replace(')', '')
|
|
8510
8510
|
.split(',');
|
|
8511
|
-
var strBorderC = rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]));
|
|
8511
|
+
var strBorderC = rgbToHex$1(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]));
|
|
8512
8512
|
cellOpts.border[idxb] = { pt: intBorderW, color: strBorderC };
|
|
8513
8513
|
});
|
|
8514
8514
|
}
|
|
@@ -12981,7 +12981,8 @@
|
|
|
12981
12981
|
bodyProperties += ' rtlCol="0"';
|
|
12982
12982
|
// D: Add anchorPoints
|
|
12983
12983
|
if (slideObject.options._bodyProp.anchor)
|
|
12984
|
-
bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"'; // VALS: [t,ctr,b]
|
|
12984
|
+
bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"'; // VALS: [t,ctr,b]
|
|
12985
|
+
if (slideObject.options._bodyProp.anchor === 'ctr') bodyProperties += ' anchorCtr=\"1\"';
|
|
12985
12986
|
if (slideObject.options._bodyProp.vert)
|
|
12986
12987
|
bodyProperties += ' vert="' + slideObject.options._bodyProp.vert + '"'; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
|
|
12987
12988
|
// E: Close <a:bodyPr element
|
|
@@ -54216,9 +54217,9 @@
|
|
|
54216
54217
|
|
|
54217
54218
|
var ttftowoff2 = {};
|
|
54218
54219
|
|
|
54219
|
-
var __dirname$1 = '/
|
|
54220
|
+
var __dirname$1 = '/Volumes/OuterHD/OuterIdeaProjects/dom-to-pptx/node_modules/fonteditor-core/woff2';
|
|
54220
54221
|
|
|
54221
|
-
var __dirname = '/
|
|
54222
|
+
var __dirname = '/Volumes/OuterHD/OuterIdeaProjects/dom-to-pptx/node_modules/fonteditor-core/woff2';
|
|
54222
54223
|
|
|
54223
54224
|
var woff2$1 = {exports: {}};
|
|
54224
54225
|
|
|
@@ -62445,9 +62446,10 @@
|
|
|
62445
62446
|
/**
|
|
62446
62447
|
* Extracts native table data for PptxGenJS.
|
|
62447
62448
|
*/
|
|
62448
|
-
function extractTableData(node, scale) {
|
|
62449
|
+
function extractTableData(node, scale, options = {}) {
|
|
62449
62450
|
const rows = [];
|
|
62450
62451
|
const colWidths = [];
|
|
62452
|
+
const root = options.root || null;
|
|
62451
62453
|
|
|
62452
62454
|
// 1. Calculate Column Widths based on the first row of cells
|
|
62453
62455
|
// We look at the first <tr>'s children to determine visual column widths.
|
|
@@ -62476,11 +62478,10 @@
|
|
|
62476
62478
|
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
62477
62479
|
|
|
62478
62480
|
// A. Text Style
|
|
62479
|
-
const textStyle = getTextStyle(style, scale);
|
|
62481
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
62480
62482
|
|
|
62481
62483
|
// B. Cell Background
|
|
62482
|
-
const
|
|
62483
|
-
const fill = (bg.hex && bg.opacity > 0) ? { color: bg.hex } : null;
|
|
62484
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
62484
62485
|
|
|
62485
62486
|
// C. Alignment
|
|
62486
62487
|
let align = 'left';
|
|
@@ -62520,6 +62521,7 @@
|
|
|
62520
62521
|
bold: textStyle.bold,
|
|
62521
62522
|
italic: textStyle.italic,
|
|
62522
62523
|
underline: textStyle.underline,
|
|
62524
|
+
lang: 'zh-CN',
|
|
62523
62525
|
|
|
62524
62526
|
fill: fill,
|
|
62525
62527
|
align: align,
|
|
@@ -62594,11 +62596,11 @@
|
|
|
62594
62596
|
// 3. Find first part that is a color (skip angle/direction)
|
|
62595
62597
|
for (const part of parts) {
|
|
62596
62598
|
// Ignore directions (to right) or angles (90deg, 0.5turn)
|
|
62597
|
-
if (/^(to\s|[\d
|
|
62599
|
+
if (/^(to\s|[\d.]+(deg|rad|turn|grad))/.test(part)) continue;
|
|
62598
62600
|
|
|
62599
62601
|
// Extract color: Remove trailing position (e.g. "red 50%" -> "red")
|
|
62600
62602
|
// Regex matches whitespace + number + unit at end of string
|
|
62601
|
-
const colorPart = part.replace(/\s+(-?[\d
|
|
62603
|
+
const colorPart = part.replace(/\s+(-?[\d.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
|
|
62602
62604
|
|
|
62603
62605
|
// Check if it's not just a number (some gradients might have bare numbers? unlikely in standard syntax)
|
|
62604
62606
|
if (colorPart) return colorPart;
|
|
@@ -62613,6 +62615,73 @@
|
|
|
62613
62615
|
return 'solid';
|
|
62614
62616
|
}
|
|
62615
62617
|
|
|
62618
|
+
function hexToRgb(hex) {
|
|
62619
|
+
const clean = (hex || '').replace('#', '').trim();
|
|
62620
|
+
if (clean.length !== 6) return null;
|
|
62621
|
+
const num = parseInt(clean, 16);
|
|
62622
|
+
if (Number.isNaN(num)) return null;
|
|
62623
|
+
return {
|
|
62624
|
+
r: (num >> 16) & 255,
|
|
62625
|
+
g: (num >> 8) & 255,
|
|
62626
|
+
b: num & 255,
|
|
62627
|
+
};
|
|
62628
|
+
}
|
|
62629
|
+
|
|
62630
|
+
function rgbToHex(rgb) {
|
|
62631
|
+
const toHex = (v) => v.toString(16).padStart(2, '0').toUpperCase();
|
|
62632
|
+
return `${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
|
|
62633
|
+
}
|
|
62634
|
+
|
|
62635
|
+
function blendHex(baseHex, overlayHex, alpha) {
|
|
62636
|
+
const base = hexToRgb(baseHex);
|
|
62637
|
+
const over = hexToRgb(overlayHex);
|
|
62638
|
+
if (!base || !over) return overlayHex;
|
|
62639
|
+
const a = Math.max(0, Math.min(1, alpha));
|
|
62640
|
+
const r = Math.round(base.r * (1 - a) + over.r * a);
|
|
62641
|
+
const g = Math.round(base.g * (1 - a) + over.g * a);
|
|
62642
|
+
const b = Math.round(base.b * (1 - a) + over.b * a);
|
|
62643
|
+
return rgbToHex({ r, g, b });
|
|
62644
|
+
}
|
|
62645
|
+
|
|
62646
|
+
function resolveTableBaseColor(node, root) {
|
|
62647
|
+
// Start from parent to avoid picking the cell's own semi-transparent fill
|
|
62648
|
+
let el = node?.parentElement || null;
|
|
62649
|
+
while (el && el !== document.body) {
|
|
62650
|
+
const style = window.getComputedStyle(el);
|
|
62651
|
+
const bg = parseColor(style.backgroundColor);
|
|
62652
|
+
if (bg.hex && bg.opacity > 0) return bg.hex;
|
|
62653
|
+
|
|
62654
|
+
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
62655
|
+
const bgImage = style.backgroundImage;
|
|
62656
|
+
if (bgClip !== 'text' && bgImage && bgImage.includes('gradient')) {
|
|
62657
|
+
const fallback = getGradientFallbackColor(bgImage);
|
|
62658
|
+
if (fallback) {
|
|
62659
|
+
const parsed = parseColor(fallback);
|
|
62660
|
+
if (parsed.hex) return parsed.hex;
|
|
62661
|
+
}
|
|
62662
|
+
}
|
|
62663
|
+
|
|
62664
|
+
if (el === root) break;
|
|
62665
|
+
el = el.parentElement;
|
|
62666
|
+
}
|
|
62667
|
+
return null;
|
|
62668
|
+
}
|
|
62669
|
+
|
|
62670
|
+
function computeTableCellFill(style, cell, root, options = {}) {
|
|
62671
|
+
const bg = parseColor(style.backgroundColor);
|
|
62672
|
+
if (!bg.hex || bg.opacity <= 0) return null;
|
|
62673
|
+
|
|
62674
|
+
const flatten = options.tableConfig?.flattenTransparentFill !== false;
|
|
62675
|
+
if (bg.opacity < 1 && flatten) {
|
|
62676
|
+
const baseHex = resolveTableBaseColor(cell, root) || 'FFFFFF';
|
|
62677
|
+
const blended = blendHex(baseHex, bg.hex, bg.opacity);
|
|
62678
|
+
return { color: blended };
|
|
62679
|
+
}
|
|
62680
|
+
|
|
62681
|
+
const transparency = Math.max(0, Math.min(100, (1 - bg.opacity) * 100));
|
|
62682
|
+
return transparency > 0 ? { color: bg.hex, transparency } : { color: bg.hex };
|
|
62683
|
+
}
|
|
62684
|
+
|
|
62616
62685
|
/**
|
|
62617
62686
|
* Analyzes computed border styles and determines the rendering strategy.
|
|
62618
62687
|
*/
|
|
@@ -62802,6 +62871,214 @@
|
|
|
62802
62871
|
return { hex, opacity: a };
|
|
62803
62872
|
}
|
|
62804
62873
|
|
|
62874
|
+
const SUPPORTED_CHART_TYPES = new Set(['area', 'bar', 'line', 'pie', 'doughnut', 'radar', 'scatter']);
|
|
62875
|
+
const CHART_TYPE_ALIASES = {
|
|
62876
|
+
column: 'bar',
|
|
62877
|
+
columnbar: 'bar',
|
|
62878
|
+
column3d: 'bar',
|
|
62879
|
+
bar3d: 'bar',
|
|
62880
|
+
donut: 'doughnut',
|
|
62881
|
+
ring: 'doughnut',
|
|
62882
|
+
};
|
|
62883
|
+
|
|
62884
|
+
function normalizeLegendPos(value) {
|
|
62885
|
+
if (!value) return null;
|
|
62886
|
+
const normalized = value.toString().toLowerCase().replace(/[^a-z]/g, '');
|
|
62887
|
+
if (normalized === 'tr' || normalized === 'topright') return 'tr';
|
|
62888
|
+
if (normalized === 'top') return 't';
|
|
62889
|
+
if (normalized === 'bottom') return 'b';
|
|
62890
|
+
if (normalized === 'left') return 'l';
|
|
62891
|
+
if (normalized === 'right') return 'r';
|
|
62892
|
+
return null;
|
|
62893
|
+
}
|
|
62894
|
+
|
|
62895
|
+
function normalizeChartType(value) {
|
|
62896
|
+
if (!value) return null;
|
|
62897
|
+
const raw = value.toString().trim().toLowerCase();
|
|
62898
|
+
const alias = CHART_TYPE_ALIASES[raw];
|
|
62899
|
+
const candidate = alias || raw;
|
|
62900
|
+
return SUPPORTED_CHART_TYPES.has(candidate) ? candidate : null;
|
|
62901
|
+
}
|
|
62902
|
+
|
|
62903
|
+
function colorToHex(value) {
|
|
62904
|
+
const parsed = parseColor(value);
|
|
62905
|
+
return parsed.hex || null;
|
|
62906
|
+
}
|
|
62907
|
+
|
|
62908
|
+
function resolveChartElement(root, config) {
|
|
62909
|
+
if (!config) return null;
|
|
62910
|
+
if (config.element instanceof HTMLElement) return config.element;
|
|
62911
|
+
if (typeof config.element === 'function') return config.element(root);
|
|
62912
|
+
if (typeof config.selector === 'string') {
|
|
62913
|
+
return root.querySelector(config.selector);
|
|
62914
|
+
}
|
|
62915
|
+
return null;
|
|
62916
|
+
}
|
|
62917
|
+
|
|
62918
|
+
function buildSeriesFromSource(source = {}) {
|
|
62919
|
+
const fallbackLabels = Array.isArray(source.labels) ? source.labels : [];
|
|
62920
|
+
const candidateDatasets = Array.isArray(source.datasets) ? source.datasets : [];
|
|
62921
|
+
const series = [];
|
|
62922
|
+
|
|
62923
|
+
candidateDatasets.forEach((dataset, index) => {
|
|
62924
|
+
if (!dataset) return;
|
|
62925
|
+
const values = Array.isArray(dataset.values)
|
|
62926
|
+
? dataset.values
|
|
62927
|
+
: Array.isArray(dataset.data)
|
|
62928
|
+
? dataset.data
|
|
62929
|
+
: [];
|
|
62930
|
+
|
|
62931
|
+
if (!values.length) return;
|
|
62932
|
+
|
|
62933
|
+
const record = {
|
|
62934
|
+
name: dataset.name || dataset.label || `Series ${index + 1}`,
|
|
62935
|
+
values,
|
|
62936
|
+
};
|
|
62937
|
+
|
|
62938
|
+
if (Array.isArray(dataset.labels) && dataset.labels.length === values.length) {
|
|
62939
|
+
record.labels = dataset.labels;
|
|
62940
|
+
} else if (fallbackLabels.length === values.length) {
|
|
62941
|
+
record.labels = fallbackLabels;
|
|
62942
|
+
}
|
|
62943
|
+
|
|
62944
|
+
if (Array.isArray(dataset.sizes) && dataset.sizes.length === values.length) {
|
|
62945
|
+
record.sizes = dataset.sizes;
|
|
62946
|
+
}
|
|
62947
|
+
|
|
62948
|
+
// Chart.js often provides strings/arrays in backgroundColor
|
|
62949
|
+
const candidateColor =
|
|
62950
|
+
dataset.color || dataset.backgroundColor || dataset.borderColor || dataset.fillColor;
|
|
62951
|
+
if (candidateColor) {
|
|
62952
|
+
if (Array.isArray(candidateColor)) {
|
|
62953
|
+
record.color = candidateColor[0];
|
|
62954
|
+
} else {
|
|
62955
|
+
record.color = candidateColor;
|
|
62956
|
+
}
|
|
62957
|
+
}
|
|
62958
|
+
|
|
62959
|
+
series.push(record);
|
|
62960
|
+
});
|
|
62961
|
+
|
|
62962
|
+
if (!series.length) {
|
|
62963
|
+
const fallbackValues = Array.isArray(source.values)
|
|
62964
|
+
? source.values
|
|
62965
|
+
: Array.isArray(source.data)
|
|
62966
|
+
? source.data
|
|
62967
|
+
: [];
|
|
62968
|
+
|
|
62969
|
+
if (fallbackValues.length) {
|
|
62970
|
+
const fallbackRecord = {
|
|
62971
|
+
name: source.name || source.label || 'Series 1',
|
|
62972
|
+
values: fallbackValues,
|
|
62973
|
+
};
|
|
62974
|
+
if (Array.isArray(source.labels) && source.labels.length === fallbackValues.length) {
|
|
62975
|
+
fallbackRecord.labels = source.labels;
|
|
62976
|
+
} else if (fallbackLabels.length === fallbackValues.length) {
|
|
62977
|
+
fallbackRecord.labels = fallbackLabels;
|
|
62978
|
+
}
|
|
62979
|
+
if (Array.isArray(source.sizes) && source.sizes.length === fallbackValues.length) {
|
|
62980
|
+
fallbackRecord.sizes = source.sizes;
|
|
62981
|
+
}
|
|
62982
|
+
if (source.color) {
|
|
62983
|
+
fallbackRecord.color = source.color;
|
|
62984
|
+
}
|
|
62985
|
+
series.push(fallbackRecord);
|
|
62986
|
+
}
|
|
62987
|
+
}
|
|
62988
|
+
|
|
62989
|
+
return series;
|
|
62990
|
+
}
|
|
62991
|
+
|
|
62992
|
+
function buildChartOptions(raw, derivedColors) {
|
|
62993
|
+
const opts = { ...((raw && raw.chartOptions) || {}) };
|
|
62994
|
+
if (raw && raw.showLegend !== undefined) opts.showLegend = raw.showLegend;
|
|
62995
|
+
if (raw) {
|
|
62996
|
+
const legendTarget = raw.legendPos || raw.legendPosition;
|
|
62997
|
+
const normalizedLegend = normalizeLegendPos(legendTarget);
|
|
62998
|
+
if (normalizedLegend) opts.legendPos = normalizedLegend;
|
|
62999
|
+
}
|
|
63000
|
+
if (raw && raw.title) opts.title = raw.title;
|
|
63001
|
+
if (raw && raw.altText) opts.altText = raw.altText;
|
|
63002
|
+
if (raw && raw.showDataTable !== undefined) opts.showDataTable = raw.showDataTable;
|
|
63003
|
+
if (raw && raw.chartColorsOpacity !== undefined) opts.chartColorsOpacity = raw.chartColorsOpacity;
|
|
63004
|
+
if (raw && raw.showLabel !== undefined) opts.showLabel = raw.showLabel;
|
|
63005
|
+
|
|
63006
|
+
const paletteFromConfig =
|
|
63007
|
+
raw && Array.isArray(raw.chartColors) ? raw.chartColors.map(colorToHex).filter(Boolean) : [];
|
|
63008
|
+
const palette = paletteFromConfig.length ? paletteFromConfig : derivedColors;
|
|
63009
|
+
if (palette.length && !opts.chartColors) {
|
|
63010
|
+
opts.chartColors = palette;
|
|
63011
|
+
}
|
|
63012
|
+
|
|
63013
|
+
return opts;
|
|
63014
|
+
}
|
|
63015
|
+
|
|
63016
|
+
function normalizeChartConfig(raw) {
|
|
63017
|
+
if (!raw) return null;
|
|
63018
|
+
const chartType = normalizeChartType(raw.chartType || raw.type || raw.chart);
|
|
63019
|
+
if (!chartType) return null;
|
|
63020
|
+
|
|
63021
|
+
let seriesWithColor = [];
|
|
63022
|
+
if (Array.isArray(raw.chartData) && raw.chartData.length) {
|
|
63023
|
+
seriesWithColor = raw.chartData
|
|
63024
|
+
.map((entry, index) => {
|
|
63025
|
+
if (!entry || !Array.isArray(entry.values)) return null;
|
|
63026
|
+
return {
|
|
63027
|
+
...entry,
|
|
63028
|
+
name: entry.name || entry.label || `Series ${index + 1}`,
|
|
63029
|
+
};
|
|
63030
|
+
})
|
|
63031
|
+
.filter((entry) => entry && entry.values && entry.values.length);
|
|
63032
|
+
} else {
|
|
63033
|
+
const source = {
|
|
63034
|
+
labels: raw.data?.labels || raw.labels,
|
|
63035
|
+
datasets: Array.isArray(raw.data?.datasets)
|
|
63036
|
+
? raw.data.datasets
|
|
63037
|
+
: Array.isArray(raw.datasets)
|
|
63038
|
+
? raw.datasets
|
|
63039
|
+
: [],
|
|
63040
|
+
values: raw.data?.values || raw.values,
|
|
63041
|
+
data: raw.data?.data,
|
|
63042
|
+
name: raw.name,
|
|
63043
|
+
label: raw.label,
|
|
63044
|
+
color: raw.color,
|
|
63045
|
+
sizes: raw.data?.sizes || raw.sizes,
|
|
63046
|
+
};
|
|
63047
|
+
seriesWithColor = buildSeriesFromSource(source);
|
|
63048
|
+
}
|
|
63049
|
+
|
|
63050
|
+
if (!seriesWithColor.length) return null;
|
|
63051
|
+
|
|
63052
|
+
const derivedColors = seriesWithColor
|
|
63053
|
+
.map((dataset) => dataset.color)
|
|
63054
|
+
.map(colorToHex)
|
|
63055
|
+
.filter(Boolean);
|
|
63056
|
+
|
|
63057
|
+
const chartOptions = buildChartOptions(raw, derivedColors);
|
|
63058
|
+
const chartData = seriesWithColor.map(({ color, ...rest }) => rest);
|
|
63059
|
+
|
|
63060
|
+
return {
|
|
63061
|
+
chartType,
|
|
63062
|
+
chartData,
|
|
63063
|
+
chartOptions,
|
|
63064
|
+
};
|
|
63065
|
+
}
|
|
63066
|
+
|
|
63067
|
+
function buildChartRegistry(root, chartConfigs = []) {
|
|
63068
|
+
const registry = new Map();
|
|
63069
|
+
if (!root || !Array.isArray(chartConfigs)) return registry;
|
|
63070
|
+
|
|
63071
|
+
chartConfigs.forEach((config) => {
|
|
63072
|
+
const node = resolveChartElement(root, config);
|
|
63073
|
+
if (!node) return;
|
|
63074
|
+
const normalized = normalizeChartConfig(config);
|
|
63075
|
+
if (!normalized || !normalized.chartData || !normalized.chartData.length) return;
|
|
63076
|
+
registry.set(node, normalized);
|
|
63077
|
+
});
|
|
63078
|
+
|
|
63079
|
+
return registry;
|
|
63080
|
+
}
|
|
63081
|
+
|
|
62805
63082
|
function getPadding(style, scale) {
|
|
62806
63083
|
const pxToInch = 1 / 96;
|
|
62807
63084
|
return [
|
|
@@ -62819,7 +63096,64 @@
|
|
|
62819
63096
|
return null;
|
|
62820
63097
|
}
|
|
62821
63098
|
|
|
62822
|
-
|
|
63099
|
+
const DEFAULT_CJK_FONTS = [
|
|
63100
|
+
'Heiti TC',
|
|
63101
|
+
'Heiti SC',
|
|
63102
|
+
'PingFang SC',
|
|
63103
|
+
'Hiragino Sans GB',
|
|
63104
|
+
'Microsoft YaHei',
|
|
63105
|
+
'Noto Sans CJK SC',
|
|
63106
|
+
'Source Han Sans SC',
|
|
63107
|
+
'WenQuanYi Micro Hei',
|
|
63108
|
+
'SimHei',
|
|
63109
|
+
'SimSun',
|
|
63110
|
+
'STHeiti'
|
|
63111
|
+
];
|
|
63112
|
+
|
|
63113
|
+
function normalizeFontList(fontFamily) {
|
|
63114
|
+
if (!fontFamily || typeof fontFamily !== 'string') return [];
|
|
63115
|
+
return fontFamily
|
|
63116
|
+
.split(',')
|
|
63117
|
+
.map((f) => f.trim().replace(/['"]/g, ''))
|
|
63118
|
+
.filter(Boolean);
|
|
63119
|
+
}
|
|
63120
|
+
|
|
63121
|
+
function containsCjk(text) {
|
|
63122
|
+
if (!text) return false;
|
|
63123
|
+
return /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/.test(text);
|
|
63124
|
+
}
|
|
63125
|
+
|
|
63126
|
+
function normalizeCjkFallbacks(options) {
|
|
63127
|
+
if (!options) return [];
|
|
63128
|
+
const raw =
|
|
63129
|
+
options.fontFallbacks?.cjk ??
|
|
63130
|
+
options.cjkFonts ??
|
|
63131
|
+
options.cjkFont ??
|
|
63132
|
+
[];
|
|
63133
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
63134
|
+
return list
|
|
63135
|
+
.map((f) => (typeof f === 'string' ? f.trim() : String(f)))
|
|
63136
|
+
.filter(Boolean);
|
|
63137
|
+
}
|
|
63138
|
+
|
|
63139
|
+
function pickFontFace(fontFamily, text, options) {
|
|
63140
|
+
const fontList = normalizeFontList(fontFamily);
|
|
63141
|
+
const primary = fontList[0] || 'Arial';
|
|
63142
|
+
|
|
63143
|
+
if (!containsCjk(text)) return primary;
|
|
63144
|
+
|
|
63145
|
+
const lowered = fontList.map((f) => f.toLowerCase());
|
|
63146
|
+
const configured = normalizeCjkFallbacks(options);
|
|
63147
|
+
if (configured.length > 0) {
|
|
63148
|
+
const match = configured.find((f) => lowered.includes(f.toLowerCase()));
|
|
63149
|
+
return match || configured[0];
|
|
63150
|
+
}
|
|
63151
|
+
|
|
63152
|
+
const autoMatch = DEFAULT_CJK_FONTS.find((f) => lowered.includes(f.toLowerCase()));
|
|
63153
|
+
return autoMatch || 'Heiti TC';
|
|
63154
|
+
}
|
|
63155
|
+
|
|
63156
|
+
function getTextStyle(style, scale, text = '', options = {}) {
|
|
62823
63157
|
let colorObj = parseColor(style.color);
|
|
62824
63158
|
|
|
62825
63159
|
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
@@ -62847,6 +63181,9 @@
|
|
|
62847
63181
|
// And apply the global layout scale.
|
|
62848
63182
|
lineSpacing = lhPx * 0.75 * scale;
|
|
62849
63183
|
}
|
|
63184
|
+
} else {
|
|
63185
|
+
// Default line height when 'normal' - use 1.2 multiplier to match browser default
|
|
63186
|
+
lineSpacing = fontSizePx * 1.2 * 0.75 * scale;
|
|
62850
63187
|
}
|
|
62851
63188
|
|
|
62852
63189
|
// --- Spacing (Margins) ---
|
|
@@ -62860,13 +63197,16 @@
|
|
|
62860
63197
|
if (mt > 0) paraSpaceBefore = mt * 0.75 * scale;
|
|
62861
63198
|
if (mb > 0) paraSpaceAfter = mb * 0.75 * scale;
|
|
62862
63199
|
|
|
63200
|
+
const fontFace = pickFontFace(style.fontFamily, text, options);
|
|
63201
|
+
|
|
62863
63202
|
return {
|
|
62864
63203
|
color: colorObj.hex || '000000',
|
|
62865
|
-
fontFace:
|
|
62866
|
-
fontSize: Math.floor(fontSizePx * 0.75 * scale),
|
|
63204
|
+
fontFace: fontFace,
|
|
63205
|
+
fontSize: Math.max(8, Math.floor(fontSizePx * 0.75 * scale)),
|
|
62867
63206
|
bold: parseInt(style.fontWeight) >= 600,
|
|
62868
63207
|
italic: style.fontStyle === 'italic',
|
|
62869
63208
|
underline: style.textDecoration.includes('underline'),
|
|
63209
|
+
lang: 'zh-CN',
|
|
62870
63210
|
// Only add if we have a valid value
|
|
62871
63211
|
...(lineSpacing && { lineSpacing }),
|
|
62872
63212
|
...(paraSpaceBefore > 0 && { paraSpaceBefore }),
|
|
@@ -62879,6 +63219,7 @@
|
|
|
62879
63219
|
/**
|
|
62880
63220
|
* Determines if a given DOM node is primarily a text container.
|
|
62881
63221
|
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
63222
|
+
* Also rejects flex/grid containers with distributed children (space-between/around/evenly).
|
|
62882
63223
|
*/
|
|
62883
63224
|
function isTextContainer(node) {
|
|
62884
63225
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -62887,6 +63228,26 @@
|
|
|
62887
63228
|
const children = Array.from(node.children);
|
|
62888
63229
|
if (children.length === 0) return true;
|
|
62889
63230
|
|
|
63231
|
+
// Check if parent is a flex/grid container with special layout
|
|
63232
|
+
// In such cases, children should be treated as separate elements
|
|
63233
|
+
const parentStyle = window.getComputedStyle(node);
|
|
63234
|
+
const display = parentStyle.display;
|
|
63235
|
+
const justifyContent = parentStyle.justifyContent || '';
|
|
63236
|
+
parentStyle.alignItems || '';
|
|
63237
|
+
|
|
63238
|
+
// If parent uses flex/grid with space distribution, don't treat as text container
|
|
63239
|
+
if ((display.includes('flex') || display.includes('grid')) &&
|
|
63240
|
+
(justifyContent.includes('space-between') ||
|
|
63241
|
+
justifyContent.includes('space-around') ||
|
|
63242
|
+
justifyContent.includes('space-evenly'))) {
|
|
63243
|
+
return false;
|
|
63244
|
+
}
|
|
63245
|
+
|
|
63246
|
+
// Note: We don't skip text container for align-items: center here.
|
|
63247
|
+
// The valign inheritance is handled in prepareRenderItem with single-child check.
|
|
63248
|
+
// This allows elements like "密钥状态流转" to properly inherit center alignment
|
|
63249
|
+
// while preventing STEP 1/2/3 from being incorrectly centered.
|
|
63250
|
+
|
|
62890
63251
|
const isSafeInline = (el) => {
|
|
62891
63252
|
// 1. Reject Web Components / Custom Elements
|
|
62892
63253
|
if (el.tagName.includes('-')) return false;
|
|
@@ -62934,6 +63295,11 @@
|
|
|
62934
63295
|
return false;
|
|
62935
63296
|
}
|
|
62936
63297
|
|
|
63298
|
+
// If element has background/border and is not inline, treat as layout block
|
|
63299
|
+
if ((hasVisibleBg || hasBorder) && !isInlineDisplay) {
|
|
63300
|
+
return false;
|
|
63301
|
+
}
|
|
63302
|
+
|
|
62937
63303
|
return true;
|
|
62938
63304
|
};
|
|
62939
63305
|
|
|
@@ -63229,10 +63595,8 @@
|
|
|
63229
63595
|
if (node.nodeType === 1) {
|
|
63230
63596
|
// Element
|
|
63231
63597
|
const style = window.getComputedStyle(node);
|
|
63232
|
-
const fontList = style.fontFamily
|
|
63233
|
-
|
|
63234
|
-
const primary = fontList[0].trim().replace(/['"]/g, '');
|
|
63235
|
-
if (primary) families.add(primary);
|
|
63598
|
+
const fontList = normalizeFontList(style.fontFamily);
|
|
63599
|
+
fontList.forEach((f) => families.add(f));
|
|
63236
63600
|
}
|
|
63237
63601
|
for (const child of node.childNodes) {
|
|
63238
63602
|
scan(child);
|
|
@@ -63442,6 +63806,60 @@
|
|
|
63442
63806
|
const PPI = 96;
|
|
63443
63807
|
const PX_TO_INCH = 1 / PPI;
|
|
63444
63808
|
|
|
63809
|
+
/**
|
|
63810
|
+
* Fix Chinese font attributes (pitchFamily and charset) in PPTX XML.
|
|
63811
|
+
* PptxGenJS sometimes generates incorrect charset="0" for East Asian fonts.
|
|
63812
|
+
* @param {Blob} blob - The PPTX blob
|
|
63813
|
+
* @returns {Promise<Blob>} - Fixed PPTX blob
|
|
63814
|
+
*/
|
|
63815
|
+
async function fixChineseFontAttributes(blob) {
|
|
63816
|
+
const zip = await JSZip.loadAsync(blob);
|
|
63817
|
+
const cjkFonts = ['Heiti TC', 'Heiti SC', 'PingFang SC', 'Microsoft YaHei', 'SimHei', 'SimSun'];
|
|
63818
|
+
|
|
63819
|
+
// Process all slide XML files
|
|
63820
|
+
const slideFiles = Object.keys(zip.files).filter(name =>
|
|
63821
|
+
name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
|
|
63822
|
+
);
|
|
63823
|
+
|
|
63824
|
+
for (const slidePath of slideFiles) {
|
|
63825
|
+
let xml = await zip.file(slidePath).async('text');
|
|
63826
|
+
let modified = false;
|
|
63827
|
+
|
|
63828
|
+
// Fix <a:ea> and <a:cs> tags for CJK fonts
|
|
63829
|
+
// Replace tags that have charset="0" (incorrect for CJK) with proper values
|
|
63830
|
+
for (const fontName of cjkFonts) {
|
|
63831
|
+
// Fix <a:ea> - East Asian font
|
|
63832
|
+
const eaPattern = new RegExp(
|
|
63833
|
+
`<a:ea typeface="${fontName}"[^>]*>`,
|
|
63834
|
+
'g'
|
|
63835
|
+
);
|
|
63836
|
+
xml = xml.replace(eaPattern, `<a:ea typeface="${fontName}" pitchFamily="34" charset="-122"/>`);
|
|
63837
|
+
|
|
63838
|
+
// Fix <a:cs> - Complex Script font
|
|
63839
|
+
const csPattern = new RegExp(
|
|
63840
|
+
`<a:cs typeface="${fontName}"[^>]*>`,
|
|
63841
|
+
'g'
|
|
63842
|
+
);
|
|
63843
|
+
xml = xml.replace(csPattern, `<a:cs typeface="${fontName}" pitchFamily="34" charset="-120"/>`);
|
|
63844
|
+
|
|
63845
|
+
// Fix <a:latin> - Latin font (if it's a CJK font used as Latin)
|
|
63846
|
+
const latinPattern = new RegExp(
|
|
63847
|
+
`<a:latin typeface="${fontName}"[^>]*>`,
|
|
63848
|
+
'g'
|
|
63849
|
+
);
|
|
63850
|
+
xml = xml.replace(latinPattern, `<a:latin typeface="${fontName}" pitchFamily="34" charset="0"/>`);
|
|
63851
|
+
}
|
|
63852
|
+
modified = true;
|
|
63853
|
+
|
|
63854
|
+
if (modified) {
|
|
63855
|
+
zip.file(slidePath, xml);
|
|
63856
|
+
}
|
|
63857
|
+
}
|
|
63858
|
+
|
|
63859
|
+
// Generate new blob
|
|
63860
|
+
return await zip.generateAsync({ type: 'blob' });
|
|
63861
|
+
}
|
|
63862
|
+
|
|
63445
63863
|
/**
|
|
63446
63864
|
* Main export function.
|
|
63447
63865
|
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
@@ -63466,6 +63884,7 @@
|
|
|
63466
63884
|
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
63467
63885
|
const pptx = new PptxConstructor();
|
|
63468
63886
|
pptx.layout = 'LAYOUT_16x9';
|
|
63887
|
+
pptx.language = 'zh-CN';
|
|
63469
63888
|
|
|
63470
63889
|
const elements = Array.isArray(target) ? target : [target];
|
|
63471
63890
|
|
|
@@ -63540,6 +63959,9 @@
|
|
|
63540
63959
|
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
63541
63960
|
}
|
|
63542
63961
|
|
|
63962
|
+
// Fix Chinese font attributes (pitchFamily and charset) in all slide XMLs
|
|
63963
|
+
finalBlob = await fixChineseFontAttributes(finalBlob);
|
|
63964
|
+
|
|
63543
63965
|
// 4. Output Handling
|
|
63544
63966
|
// If skipDownload is NOT true, proceed with browser download
|
|
63545
63967
|
if (!options.skipDownload) {
|
|
@@ -63581,6 +64003,9 @@
|
|
|
63581
64003
|
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
63582
64004
|
};
|
|
63583
64005
|
|
|
64006
|
+
const chartRegistry = buildChartRegistry(root, globalOptions.chartConfigs);
|
|
64007
|
+
const slideOptions = { ...globalOptions, chartRegistry };
|
|
64008
|
+
|
|
63584
64009
|
const renderQueue = [];
|
|
63585
64010
|
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
63586
64011
|
let domOrderCounter = 0;
|
|
@@ -63616,7 +64041,7 @@
|
|
|
63616
64041
|
pptx,
|
|
63617
64042
|
currentZ,
|
|
63618
64043
|
nodeStyle,
|
|
63619
|
-
|
|
64044
|
+
slideOptions
|
|
63620
64045
|
);
|
|
63621
64046
|
|
|
63622
64047
|
if (result) {
|
|
@@ -63662,6 +64087,10 @@
|
|
|
63662
64087
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
63663
64088
|
if (item.type === 'image') slide.addImage(item.options);
|
|
63664
64089
|
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
64090
|
+
if (item.type === 'chart') {
|
|
64091
|
+
const chartType = PptxGenJS.ChartType?.[item.chartType] ?? item.chartType;
|
|
64092
|
+
slide.addChart(chartType, item.chartData, item.options);
|
|
64093
|
+
}
|
|
63665
64094
|
if (item.type === 'table') {
|
|
63666
64095
|
slide.addTable(item.tableData.rows, {
|
|
63667
64096
|
x: item.options.x,
|
|
@@ -63862,13 +64291,23 @@
|
|
|
63862
64291
|
range.detach();
|
|
63863
64292
|
|
|
63864
64293
|
const style = window.getComputedStyle(parent);
|
|
63865
|
-
|
|
63866
|
-
|
|
64294
|
+
// Use parent element's rect for better width calculation
|
|
64295
|
+
// This is especially important for flex children where text node rect may be too narrow
|
|
64296
|
+
const parentRect = parent.getBoundingClientRect();
|
|
64297
|
+
const useParentRect = parentRect.width > rect.width;
|
|
64298
|
+
let widthPx = useParentRect ? parentRect.width : rect.width;
|
|
64299
|
+
const heightPx = useParentRect ? parentRect.height : rect.height;
|
|
64300
|
+
|
|
64301
|
+
// Add extra width buffer to prevent text wrapping in PPTX due to font rendering differences
|
|
64302
|
+
// Chinese characters and certain fonts need more space in PPTX than in browser
|
|
64303
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
63867
64304
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
63868
64305
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
63869
64306
|
|
|
63870
|
-
|
|
63871
|
-
const
|
|
64307
|
+
// Use parent rect's position if we're using parent dimensions
|
|
64308
|
+
const sourceRect = useParentRect ? parentRect : rect;
|
|
64309
|
+
const x = config.offX + (sourceRect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
64310
|
+
const y = config.offY + (sourceRect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
63872
64311
|
|
|
63873
64312
|
return {
|
|
63874
64313
|
items: [
|
|
@@ -63879,7 +64318,14 @@
|
|
|
63879
64318
|
textParts: [
|
|
63880
64319
|
{
|
|
63881
64320
|
text: textContent,
|
|
63882
|
-
options:
|
|
64321
|
+
options: (() => {
|
|
64322
|
+
const opts = getTextStyle(style, config.scale, textContent, globalOptions);
|
|
64323
|
+
const bg = parseColor(style.backgroundColor);
|
|
64324
|
+
if (opts.highlight && bg.hex && bg.opacity > 0 && !isTextContainer(parent)) {
|
|
64325
|
+
delete opts.highlight;
|
|
64326
|
+
}
|
|
64327
|
+
return opts;
|
|
64328
|
+
})(),
|
|
63883
64329
|
},
|
|
63884
64330
|
],
|
|
63885
64331
|
options: { x, y, w: unrotatedW, h: unrotatedH, margin: 0, autoFit: false },
|
|
@@ -63900,7 +64346,7 @@
|
|
|
63900
64346
|
const elementOpacity = parseFloat(style.opacity);
|
|
63901
64347
|
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
63902
64348
|
|
|
63903
|
-
|
|
64349
|
+
let widthPx = node.offsetWidth || rect.width;
|
|
63904
64350
|
const heightPx = node.offsetHeight || rect.height;
|
|
63905
64351
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
63906
64352
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
@@ -63914,13 +64360,75 @@
|
|
|
63914
64360
|
|
|
63915
64361
|
const items = [];
|
|
63916
64362
|
|
|
63917
|
-
|
|
63918
|
-
|
|
64363
|
+
const chartConfig = globalOptions.chartRegistry?.get(node);
|
|
64364
|
+
if (chartConfig) {
|
|
64365
|
+
const chartOptions = {
|
|
64366
|
+
...chartConfig.chartOptions,
|
|
64367
|
+
x,
|
|
64368
|
+
y,
|
|
64369
|
+
w: unrotatedW,
|
|
64370
|
+
h: unrotatedH,
|
|
64371
|
+
};
|
|
64372
|
+
|
|
64373
|
+
return {
|
|
64374
|
+
items: [
|
|
64375
|
+
{
|
|
64376
|
+
type: 'chart',
|
|
64377
|
+
zIndex: effectiveZIndex,
|
|
64378
|
+
domOrder,
|
|
64379
|
+
chartType: chartConfig.chartType,
|
|
64380
|
+
chartData: chartConfig.chartData,
|
|
64381
|
+
options: chartOptions,
|
|
64382
|
+
},
|
|
64383
|
+
],
|
|
64384
|
+
stopRecursion: true,
|
|
64385
|
+
};
|
|
64386
|
+
}
|
|
64387
|
+
|
|
64388
|
+
// --- Handle both native TABLE and div-based table structures ---
|
|
64389
|
+
const isTableLike = node.tagName === 'TABLE' || detectTableLikeStructure(node);
|
|
64390
|
+
if (isTableLike) {
|
|
64391
|
+
let tableData;
|
|
64392
|
+
if (node.tagName === 'TABLE') {
|
|
64393
|
+
tableData = extractTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
64394
|
+
} else {
|
|
64395
|
+
// Extract data from div-based table structure
|
|
64396
|
+
tableData = extractDivTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
64397
|
+
}
|
|
64398
|
+
const cellBgItems = [];
|
|
64399
|
+
const renderCellBg = globalOptions.tableConfig?.renderCellBackgrounds !== false;
|
|
64400
|
+
|
|
64401
|
+
if (renderCellBg) {
|
|
64402
|
+
const trList = node.querySelectorAll('tr');
|
|
64403
|
+
trList.forEach((tr) => {
|
|
64404
|
+
const cellList = Array.from(tr.children).filter((c) => ['TD', 'TH'].includes(c.tagName));
|
|
64405
|
+
cellList.forEach((cell) => {
|
|
64406
|
+
const style = window.getComputedStyle(cell);
|
|
64407
|
+
const fill = computeTableCellFill(style, cell, config.root, globalOptions);
|
|
64408
|
+
if (!fill) return;
|
|
64409
|
+
|
|
64410
|
+
const rect = cell.getBoundingClientRect();
|
|
64411
|
+
const wIn = rect.width * PX_TO_INCH * config.scale;
|
|
64412
|
+
const hIn = rect.height * PX_TO_INCH * config.scale;
|
|
64413
|
+
const xIn = config.offX + (rect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
64414
|
+
const yIn = config.offY + (rect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
64415
|
+
|
|
64416
|
+
cellBgItems.push({
|
|
64417
|
+
type: 'shape',
|
|
64418
|
+
zIndex: effectiveZIndex - 0.5,
|
|
64419
|
+
domOrder,
|
|
64420
|
+
shapeType: 'rect',
|
|
64421
|
+
options: { x: xIn, y: yIn, w: wIn, h: hIn, fill: fill, line: { color: 'FFFFFF', transparency: 100 } }
|
|
64422
|
+
});
|
|
64423
|
+
});
|
|
64424
|
+
});
|
|
64425
|
+
}
|
|
63919
64426
|
|
|
63920
64427
|
// Calculate total table width to ensure X position is correct
|
|
63921
64428
|
// (Though x calculation above usually handles it, tables can be finicky)
|
|
63922
64429
|
return {
|
|
63923
64430
|
items: [
|
|
64431
|
+
...cellBgItems,
|
|
63924
64432
|
{
|
|
63925
64433
|
type: 'table',
|
|
63926
64434
|
zIndex: effectiveZIndex,
|
|
@@ -64012,7 +64520,7 @@
|
|
|
64012
64520
|
}
|
|
64013
64521
|
|
|
64014
64522
|
// 3. Extract Text Parts
|
|
64015
|
-
const parts = collectListParts(child, liStyle, config.scale);
|
|
64523
|
+
const parts = collectListParts(child, liStyle, config.scale, globalOptions);
|
|
64016
64524
|
|
|
64017
64525
|
if (parts.length > 0) {
|
|
64018
64526
|
parts.forEach((p) => {
|
|
@@ -64230,6 +64738,35 @@
|
|
|
64230
64738
|
return { items: [item], job, stopRecursion: true };
|
|
64231
64739
|
}
|
|
64232
64740
|
|
|
64741
|
+
// --- Handle vertical stat cards (like .mini-stat with number + label) ---
|
|
64742
|
+
if (isVerticalStatCard(node)) {
|
|
64743
|
+
// Capture the entire stat card as an image to preserve vertical layout
|
|
64744
|
+
const item = {
|
|
64745
|
+
type: 'image',
|
|
64746
|
+
zIndex,
|
|
64747
|
+
domOrder,
|
|
64748
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
64749
|
+
};
|
|
64750
|
+
const job = async () => {
|
|
64751
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
64752
|
+
if (pngData) item.options.data = pngData;
|
|
64753
|
+
else item.skip = true;
|
|
64754
|
+
};
|
|
64755
|
+
return { items: [item], job, stopRecursion: true };
|
|
64756
|
+
}
|
|
64757
|
+
|
|
64758
|
+
// --- Handle absolutely positioned children that overflow parent ---
|
|
64759
|
+
// Check if this element has absolutely positioned children that extend outside
|
|
64760
|
+
const overflowingChildren = detectOverflowingChildren(node);
|
|
64761
|
+
if (overflowingChildren.length > 0) {
|
|
64762
|
+
// Process this node normally, but also capture overflowing children separately
|
|
64763
|
+
const baseResult = processOverflowingContent(
|
|
64764
|
+
node, overflowingChildren, config, domOrder, pptx, zIndex);
|
|
64765
|
+
if (baseResult) {
|
|
64766
|
+
return baseResult;
|
|
64767
|
+
}
|
|
64768
|
+
}
|
|
64769
|
+
|
|
64233
64770
|
// Radii logic
|
|
64234
64771
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
64235
64772
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
@@ -64332,6 +64869,27 @@
|
|
|
64332
64869
|
let textPayload = null;
|
|
64333
64870
|
const isText = isTextContainer(node);
|
|
64334
64871
|
|
|
64872
|
+
// Add extra width buffer for inline text elements to prevent wrapping in PPTX
|
|
64873
|
+
// This is especially needed for Chinese text and certain fonts
|
|
64874
|
+
// Only apply to elements that are direct children of flex containers with space distribution
|
|
64875
|
+
// and don't have a parent with visible background (to avoid overflowing card boundaries)
|
|
64876
|
+
if (isText && !bgColorObj.hex && !hasBorder && !hasShadow) {
|
|
64877
|
+
const parentEl = node.parentElement;
|
|
64878
|
+
if (parentEl) {
|
|
64879
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
64880
|
+
const parentDisplay = parentStyle.display || '';
|
|
64881
|
+
const parentJustify = parentStyle.justifyContent || '';
|
|
64882
|
+
const isFlexWithSpaceDist = (parentDisplay.includes('flex') || parentDisplay.includes('grid')) &&
|
|
64883
|
+
(parentJustify.includes('space-between') || parentJustify.includes('space-around') || parentJustify.includes('space-evenly'));
|
|
64884
|
+
|
|
64885
|
+
if (isFlexWithSpaceDist) {
|
|
64886
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
64887
|
+
// Recalculate w with the new width
|
|
64888
|
+
w = widthPx * PX_TO_INCH * config.scale;
|
|
64889
|
+
}
|
|
64890
|
+
}
|
|
64891
|
+
}
|
|
64892
|
+
|
|
64335
64893
|
if (isText) {
|
|
64336
64894
|
const textParts = [];
|
|
64337
64895
|
let trimNextLeading = false;
|
|
@@ -64370,7 +64928,7 @@
|
|
|
64370
64928
|
if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
|
|
64371
64929
|
|
|
64372
64930
|
if (textVal.length > 0) {
|
|
64373
|
-
const textOpts = getTextStyle(nodeStyle, config.scale);
|
|
64931
|
+
const textOpts = getTextStyle(nodeStyle, config.scale, textVal, globalOptions);
|
|
64374
64932
|
|
|
64375
64933
|
// BUG FIX: Numbers 1 and 2 having background.
|
|
64376
64934
|
// If this is a naked Text Node (nodeType 3), it inherits style from the parent container.
|
|
@@ -64388,8 +64946,71 @@
|
|
|
64388
64946
|
let align = style.textAlign || 'left';
|
|
64389
64947
|
if (align === 'start') align = 'left';
|
|
64390
64948
|
if (align === 'end') align = 'right';
|
|
64949
|
+
|
|
64950
|
+
// Fix: If this element is a flex/grid child and has no explicit text-align,
|
|
64951
|
+
// force left alignment to match HTML default behavior
|
|
64952
|
+
if (node.parentElement && (!style.textAlign || style.textAlign === 'start')) {
|
|
64953
|
+
const parentStyle = window.getComputedStyle(node.parentElement);
|
|
64954
|
+
if (parentStyle.display.includes('flex') || parentStyle.display.includes('grid')) {
|
|
64955
|
+
align = 'left';
|
|
64956
|
+
}
|
|
64957
|
+
}
|
|
64958
|
+
|
|
64959
|
+
// Detect badge/pill buttons (high border-radius + short text) and auto-center them
|
|
64960
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
64961
|
+
const height = parseFloat(style.height) || node.offsetHeight;
|
|
64962
|
+
const textContent = node.textContent.trim();
|
|
64963
|
+
const className = (node.className || '').toLowerCase();
|
|
64964
|
+
|
|
64965
|
+
// Real badges/pills typically have visible styling (background, border, or large border-radius)
|
|
64966
|
+
const hasVisibleBackground = bgColorObj.hex && bgColorObj.opacity > 0.1;
|
|
64967
|
+
const hasVisibleBorder = borderWidth > 0;
|
|
64968
|
+
const hasLargeBorderRadius = borderRadius >= height / 2;
|
|
64969
|
+
const hasBadgeClass = className.includes('badge') || className.includes('pill');
|
|
64970
|
+
|
|
64971
|
+
// Only consider it a badge if it has visual styling AND short text
|
|
64972
|
+
const isLikelyBadge =
|
|
64973
|
+
(hasLargeBorderRadius || hasBadgeClass) &&
|
|
64974
|
+
textContent.length <= 10 &&
|
|
64975
|
+
(hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius);
|
|
64976
|
+
|
|
64977
|
+
if (isLikelyBadge) {
|
|
64978
|
+
align = 'center';
|
|
64979
|
+
}
|
|
64980
|
+
|
|
64391
64981
|
let valign = 'top';
|
|
64392
|
-
|
|
64982
|
+
|
|
64983
|
+
// For flex items, valign should be determined by PARENT's align-items, not self
|
|
64984
|
+
// Self's align-items controls how children are aligned, not self's position in parent
|
|
64985
|
+
const parentEl = node.parentElement;
|
|
64986
|
+
if (parentEl) {
|
|
64987
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
64988
|
+
if (parentStyle.display.includes('flex')) {
|
|
64989
|
+
// Parent's align-items controls this element's cross-axis alignment
|
|
64990
|
+
if (parentStyle.alignItems === 'center') {
|
|
64991
|
+
valign = 'middle';
|
|
64992
|
+
} else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
|
|
64993
|
+
valign = 'bottom';
|
|
64994
|
+
}
|
|
64995
|
+
// Default (stretch, flex-start) keeps valign = 'top'
|
|
64996
|
+
}
|
|
64997
|
+
}
|
|
64998
|
+
|
|
64999
|
+
// If element is not a flex item (no flex parent), then its own align-items
|
|
65000
|
+
// might indicate self-centering intent for single-element containers
|
|
65001
|
+
if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
|
|
65002
|
+
valign = 'middle';
|
|
65003
|
+
}
|
|
65004
|
+
|
|
65005
|
+
// Auto-center vertically for likely badges
|
|
65006
|
+
if (isLikelyBadge && valign !== 'middle') {
|
|
65007
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
65008
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
65009
|
+
// If padding is relatively even, assume vertical centering is intended
|
|
65010
|
+
if (Math.abs(paddingTop - paddingBottom) <= 4) {
|
|
65011
|
+
valign = 'middle';
|
|
65012
|
+
}
|
|
65013
|
+
}
|
|
64393
65014
|
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
64394
65015
|
|
|
64395
65016
|
const pt = parseFloat(style.paddingTop) || 0;
|
|
@@ -64613,6 +65234,8 @@
|
|
|
64613
65234
|
|
|
64614
65235
|
function isComplexHierarchy(root) {
|
|
64615
65236
|
// Use a simple tree traversal to find forbidden elements in the list structure
|
|
65237
|
+
if (root?.getAttribute?.('data-pptx-list') === 'complex') return true;
|
|
65238
|
+
|
|
64616
65239
|
const stack = [root];
|
|
64617
65240
|
while (stack.length > 0) {
|
|
64618
65241
|
const el = stack.pop();
|
|
@@ -64621,6 +65244,27 @@
|
|
|
64621
65244
|
if (el.tagName === 'LI') {
|
|
64622
65245
|
const s = window.getComputedStyle(el);
|
|
64623
65246
|
if (s.display === 'flex' || s.display === 'grid' || s.display === 'inline-flex') return true;
|
|
65247
|
+
|
|
65248
|
+
// Custom list items (e.g., list-style: none + structured children)
|
|
65249
|
+
const listStyleType = s.listStyleType || s.listStyle;
|
|
65250
|
+
if (listStyleType === 'none') {
|
|
65251
|
+
const hasStructuredChild = Array.from(el.children).some((child) => {
|
|
65252
|
+
const cs = window.getComputedStyle(child);
|
|
65253
|
+
const display = cs.display || '';
|
|
65254
|
+
if (!display.includes('inline')) return true;
|
|
65255
|
+
|
|
65256
|
+
const bg = parseColor(cs.backgroundColor);
|
|
65257
|
+
if (bg.hex && bg.opacity > 0) return true;
|
|
65258
|
+
|
|
65259
|
+
const bw = parseFloat(cs.borderWidth) || 0;
|
|
65260
|
+
const bc = parseColor(cs.borderColor);
|
|
65261
|
+
if (bw > 0 && bc.opacity > 0) return true;
|
|
65262
|
+
|
|
65263
|
+
return false;
|
|
65264
|
+
});
|
|
65265
|
+
|
|
65266
|
+
if (hasStructuredChild) return true;
|
|
65267
|
+
}
|
|
64624
65268
|
}
|
|
64625
65269
|
|
|
64626
65270
|
// 2. Media / Icons
|
|
@@ -64638,7 +65282,7 @@
|
|
|
64638
65282
|
return false;
|
|
64639
65283
|
}
|
|
64640
65284
|
|
|
64641
|
-
function collectListParts(node, parentStyle, scale) {
|
|
65285
|
+
function collectListParts(node, parentStyle, scale, globalOptions) {
|
|
64642
65286
|
const parts = [];
|
|
64643
65287
|
|
|
64644
65288
|
// Check for CSS Content (::before) - often used for icons
|
|
@@ -64651,7 +65295,7 @@
|
|
|
64651
65295
|
if (cleanContent.trim()) {
|
|
64652
65296
|
parts.push({
|
|
64653
65297
|
text: cleanContent + ' ', // Add space after icon
|
|
64654
|
-
options: getTextStyle(window.getComputedStyle(node), scale),
|
|
65298
|
+
options: getTextStyle(window.getComputedStyle(node), scale, cleanContent, globalOptions),
|
|
64655
65299
|
});
|
|
64656
65300
|
}
|
|
64657
65301
|
}
|
|
@@ -64666,13 +65310,13 @@
|
|
|
64666
65310
|
const styleToUse = node.nodeType === 1 ? window.getComputedStyle(node) : parentStyle;
|
|
64667
65311
|
parts.push({
|
|
64668
65312
|
text: val,
|
|
64669
|
-
options: getTextStyle(styleToUse, scale),
|
|
65313
|
+
options: getTextStyle(styleToUse, scale, val, globalOptions),
|
|
64670
65314
|
});
|
|
64671
65315
|
}
|
|
64672
65316
|
} else if (child.nodeType === 1) {
|
|
64673
65317
|
// Element (span, i, b)
|
|
64674
65318
|
// Recurse
|
|
64675
|
-
parts.push(...collectListParts(child, parentStyle, scale));
|
|
65319
|
+
parts.push(...collectListParts(child, parentStyle, scale, globalOptions));
|
|
64676
65320
|
}
|
|
64677
65321
|
});
|
|
64678
65322
|
|
|
@@ -64726,6 +65370,283 @@
|
|
|
64726
65370
|
return items;
|
|
64727
65371
|
}
|
|
64728
65372
|
|
|
65373
|
+
/**
|
|
65374
|
+
* Detect absolutely positioned children that overflow their parent
|
|
65375
|
+
* This handles cases like chart value labels positioned above bars
|
|
65376
|
+
*/
|
|
65377
|
+
function detectOverflowingChildren(node) {
|
|
65378
|
+
const overflowing = [];
|
|
65379
|
+
const parentRect = node.getBoundingClientRect();
|
|
65380
|
+
|
|
65381
|
+
// Only check for elements with specific class patterns that suggest chart values
|
|
65382
|
+
const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
|
|
65383
|
+
|
|
65384
|
+
children.forEach(child => {
|
|
65385
|
+
const childStyle = window.getComputedStyle(child);
|
|
65386
|
+
const childRect = child.getBoundingClientRect();
|
|
65387
|
+
|
|
65388
|
+
// Check if absolutely positioned and outside parent bounds
|
|
65389
|
+
if (childStyle.position === 'absolute') {
|
|
65390
|
+
const isOutside =
|
|
65391
|
+
childRect.bottom < parentRect.top ||
|
|
65392
|
+
childRect.top > parentRect.bottom ||
|
|
65393
|
+
childRect.right < parentRect.left ||
|
|
65394
|
+
childRect.left > parentRect.right;
|
|
65395
|
+
|
|
65396
|
+
if (isOutside || childRect.top < parentRect.top) {
|
|
65397
|
+
overflowing.push(child);
|
|
65398
|
+
}
|
|
65399
|
+
}
|
|
65400
|
+
});
|
|
65401
|
+
|
|
65402
|
+
return overflowing;
|
|
65403
|
+
}
|
|
65404
|
+
|
|
65405
|
+
/**
|
|
65406
|
+
* Process content with overflowing children
|
|
65407
|
+
* Captures the entire visual area including overflowing elements
|
|
65408
|
+
*/
|
|
65409
|
+
function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
|
|
65410
|
+
// Calculate bounding box that includes all overflowing children
|
|
65411
|
+
const rect = node.getBoundingClientRect();
|
|
65412
|
+
let minX = rect.left;
|
|
65413
|
+
let minY = rect.top;
|
|
65414
|
+
let maxX = rect.right;
|
|
65415
|
+
let maxY = rect.bottom;
|
|
65416
|
+
|
|
65417
|
+
overflowingChildren.forEach(child => {
|
|
65418
|
+
const childRect = child.getBoundingClientRect();
|
|
65419
|
+
minX = Math.min(minX, childRect.left);
|
|
65420
|
+
minY = Math.min(minY, childRect.top);
|
|
65421
|
+
maxX = Math.max(maxX, childRect.right);
|
|
65422
|
+
maxY = Math.max(maxY, childRect.bottom);
|
|
65423
|
+
});
|
|
65424
|
+
|
|
65425
|
+
const totalWidth = maxX - minX;
|
|
65426
|
+
const totalHeight = maxY - minY;
|
|
65427
|
+
|
|
65428
|
+
// Use html2canvas to capture the entire area
|
|
65429
|
+
const item = {
|
|
65430
|
+
type: 'image',
|
|
65431
|
+
zIndex,
|
|
65432
|
+
domOrder,
|
|
65433
|
+
options: {
|
|
65434
|
+
x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
|
|
65435
|
+
y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
|
|
65436
|
+
w: totalWidth * PX_TO_INCH * config.scale,
|
|
65437
|
+
h: totalHeight * PX_TO_INCH * config.scale,
|
|
65438
|
+
data: null
|
|
65439
|
+
}
|
|
65440
|
+
};
|
|
65441
|
+
|
|
65442
|
+
const job = async () => {
|
|
65443
|
+
try {
|
|
65444
|
+
// Temporarily adjust node position for capture
|
|
65445
|
+
const originalPosition = node.style.position;
|
|
65446
|
+
const originalTransform = node.style.transform;
|
|
65447
|
+
|
|
65448
|
+
node.style.position = 'relative';
|
|
65449
|
+
node.style.transform = 'none';
|
|
65450
|
+
|
|
65451
|
+
const canvas = await html2canvas(node, {
|
|
65452
|
+
backgroundColor: null,
|
|
65453
|
+
logging: false,
|
|
65454
|
+
scale: 2,
|
|
65455
|
+
useCORS: true,
|
|
65456
|
+
x: minX - rect.left,
|
|
65457
|
+
y: minY - rect.top,
|
|
65458
|
+
width: totalWidth,
|
|
65459
|
+
height: totalHeight
|
|
65460
|
+
});
|
|
65461
|
+
|
|
65462
|
+
// Restore original styles
|
|
65463
|
+
node.style.position = originalPosition;
|
|
65464
|
+
node.style.transform = originalTransform;
|
|
65465
|
+
|
|
65466
|
+
item.options.data = canvas.toDataURL('image/png');
|
|
65467
|
+
} catch (e) {
|
|
65468
|
+
console.warn('Failed to capture overflowing content:', e);
|
|
65469
|
+
item.skip = true;
|
|
65470
|
+
}
|
|
65471
|
+
};
|
|
65472
|
+
|
|
65473
|
+
return { items: [item], job, stopRecursion: true };
|
|
65474
|
+
}
|
|
65475
|
+
|
|
65476
|
+
/**
|
|
65477
|
+
* Detect vertical stat cards (like .mini-stat with number above label)
|
|
65478
|
+
* These have block-level children that should stack vertically
|
|
65479
|
+
*/
|
|
65480
|
+
function isVerticalStatCard(node) {
|
|
65481
|
+
const className = (node.className || '').toLowerCase();
|
|
65482
|
+
|
|
65483
|
+
// Check for stat-like class names
|
|
65484
|
+
const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
|
|
65485
|
+
const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
|
|
65486
|
+
|
|
65487
|
+
if (!hasStatClass) return false;
|
|
65488
|
+
|
|
65489
|
+
const children = Array.from(node.children);
|
|
65490
|
+
if (children.length !== 2) return false;
|
|
65491
|
+
|
|
65492
|
+
// Check if children are likely number + label pair
|
|
65493
|
+
const child1 = children[0];
|
|
65494
|
+
const child2 = children[1];
|
|
65495
|
+
|
|
65496
|
+
const style1 = window.getComputedStyle(child1);
|
|
65497
|
+
const style2 = window.getComputedStyle(child2);
|
|
65498
|
+
|
|
65499
|
+
// Both should be block elements (or have block-like display)
|
|
65500
|
+
const isBlock1 = style1.display === 'block' || style1.display === 'flex';
|
|
65501
|
+
const isBlock2 = style2.display === 'block' || style2.display === 'flex';
|
|
65502
|
+
|
|
65503
|
+
if (!isBlock1 || !isBlock2) return false;
|
|
65504
|
+
|
|
65505
|
+
// First child should have larger font (the number)
|
|
65506
|
+
const fontSize1 = parseFloat(style1.fontSize) || 0;
|
|
65507
|
+
const fontSize2 = parseFloat(style2.fontSize) || 0;
|
|
65508
|
+
|
|
65509
|
+
// Number should be larger than label, or at least bold
|
|
65510
|
+
const isBold1 = parseInt(style1.fontWeight) >= 600;
|
|
65511
|
+
|
|
65512
|
+
return fontSize1 >= fontSize2 || isBold1;
|
|
65513
|
+
}
|
|
65514
|
+
|
|
65515
|
+
/**
|
|
65516
|
+
* Detect if a div structure looks like a table
|
|
65517
|
+
* Checks for table-like class names and structure patterns
|
|
65518
|
+
*/
|
|
65519
|
+
function detectTableLikeStructure(node) {
|
|
65520
|
+
const className = (node.className || '').toLowerCase();
|
|
65521
|
+
|
|
65522
|
+
// Check for table-related class names
|
|
65523
|
+
const tableIndicators = ['table', 'data-table', 'grid', 'list'];
|
|
65524
|
+
const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
|
|
65525
|
+
|
|
65526
|
+
if (!hasTableClass) return false;
|
|
65527
|
+
|
|
65528
|
+
// Check structure: should have header row and data rows
|
|
65529
|
+
const children = Array.from(node.children);
|
|
65530
|
+
|
|
65531
|
+
// Look for header-like element
|
|
65532
|
+
const hasHeader = children.some(child =>
|
|
65533
|
+
(child.className || '').toLowerCase().includes('header') ||
|
|
65534
|
+
child.tagName === 'THEAD'
|
|
65535
|
+
);
|
|
65536
|
+
|
|
65537
|
+
// Look for row-like elements
|
|
65538
|
+
const hasRows = children.some(child =>
|
|
65539
|
+
(child.className || '').toLowerCase().includes('row') ||
|
|
65540
|
+
child.tagName === 'TR' ||
|
|
65541
|
+
child.tagName === 'TBODY'
|
|
65542
|
+
);
|
|
65543
|
+
|
|
65544
|
+
// Check for cell-like structure in children
|
|
65545
|
+
const hasCellStructure = children.some(child => {
|
|
65546
|
+
const childChildren = Array.from(child.children);
|
|
65547
|
+
return childChildren.some(grandchild =>
|
|
65548
|
+
(grandchild.className || '').toLowerCase().includes('cell') ||
|
|
65549
|
+
grandchild.className.toLowerCase().includes('col') ||
|
|
65550
|
+
grandchild.className.toLowerCase().includes('td')
|
|
65551
|
+
);
|
|
65552
|
+
});
|
|
65553
|
+
|
|
65554
|
+
return (hasHeader || hasRows) && hasCellStructure;
|
|
65555
|
+
}
|
|
65556
|
+
|
|
65557
|
+
/**
|
|
65558
|
+
* Extract table data from div-based table structure
|
|
65559
|
+
* Similar to extractTableData but for div grids
|
|
65560
|
+
*/
|
|
65561
|
+
function extractDivTableData(node, scale, options = {}) {
|
|
65562
|
+
const rows = [];
|
|
65563
|
+
const colWidths = [];
|
|
65564
|
+
const root = options.root || null;
|
|
65565
|
+
|
|
65566
|
+
// Find header row
|
|
65567
|
+
const headerRow = node.querySelector('.table-header, [class*="header"]');
|
|
65568
|
+
if (headerRow) {
|
|
65569
|
+
const headerCells = Array.from(headerRow.children);
|
|
65570
|
+
headerCells.forEach((cell, index) => {
|
|
65571
|
+
const rect = cell.getBoundingClientRect();
|
|
65572
|
+
const wIn = rect.width * (1 / 96) * scale;
|
|
65573
|
+
colWidths[index] = wIn;
|
|
65574
|
+
});
|
|
65575
|
+
|
|
65576
|
+
// Process header as first row
|
|
65577
|
+
const headerData = headerCells.map(cell => {
|
|
65578
|
+
const style = window.getComputedStyle(cell);
|
|
65579
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
65580
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
65581
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
65582
|
+
|
|
65583
|
+
return {
|
|
65584
|
+
text: cellText,
|
|
65585
|
+
options: {
|
|
65586
|
+
...textStyle,
|
|
65587
|
+
fill: fill || { color: '16A085', transparency: 80 },
|
|
65588
|
+
bold: true,
|
|
65589
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
65590
|
+
border: { type: 'none' }
|
|
65591
|
+
}
|
|
65592
|
+
};
|
|
65593
|
+
});
|
|
65594
|
+
rows.push(headerData);
|
|
65595
|
+
}
|
|
65596
|
+
|
|
65597
|
+
// Find data rows
|
|
65598
|
+
const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
|
|
65599
|
+
dataRows.forEach(row => {
|
|
65600
|
+
const cells = Array.from(row.children);
|
|
65601
|
+
const rowData = cells.map(cell => {
|
|
65602
|
+
const style = window.getComputedStyle(cell);
|
|
65603
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
65604
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
65605
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
65606
|
+
|
|
65607
|
+
// Detect trend colors
|
|
65608
|
+
const className = (cell.className || '').toLowerCase();
|
|
65609
|
+
let textColor = textStyle.color;
|
|
65610
|
+
if (className.includes('up') || className.includes('positive')) {
|
|
65611
|
+
textColor = '16A085';
|
|
65612
|
+
} else if (className.includes('down') || className.includes('negative')) {
|
|
65613
|
+
textColor = 'E74C3C';
|
|
65614
|
+
}
|
|
65615
|
+
|
|
65616
|
+
return {
|
|
65617
|
+
text: cellText,
|
|
65618
|
+
options: {
|
|
65619
|
+
...textStyle,
|
|
65620
|
+
color: textColor,
|
|
65621
|
+
fill: fill,
|
|
65622
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
65623
|
+
border: {
|
|
65624
|
+
type: 'none',
|
|
65625
|
+
bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
|
|
65626
|
+
}
|
|
65627
|
+
}
|
|
65628
|
+
};
|
|
65629
|
+
});
|
|
65630
|
+
|
|
65631
|
+
if (rowData.length > 0) {
|
|
65632
|
+
rows.push(rowData);
|
|
65633
|
+
}
|
|
65634
|
+
});
|
|
65635
|
+
|
|
65636
|
+
// Ensure all rows have same column count
|
|
65637
|
+
const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
|
|
65638
|
+
rows.forEach(row => {
|
|
65639
|
+
while (row.length < maxCols) {
|
|
65640
|
+
row.push({ text: '', options: {} });
|
|
65641
|
+
}
|
|
65642
|
+
});
|
|
65643
|
+
while (colWidths.length < maxCols) {
|
|
65644
|
+
colWidths.push(colWidths[colWidths.length - 1] || 1);
|
|
65645
|
+
}
|
|
65646
|
+
|
|
65647
|
+
return { rows, colWidths };
|
|
65648
|
+
}
|
|
65649
|
+
|
|
64729
65650
|
exports.exportToPptx = exportToPptx;
|
|
64730
65651
|
|
|
64731
65652
|
}));
|