@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
package/dist/dom-to-pptx.cjs
CHANGED
|
@@ -339,6 +339,7 @@ function extractTableData(node, scale, options = {}) {
|
|
|
339
339
|
bold: textStyle.bold,
|
|
340
340
|
italic: textStyle.italic,
|
|
341
341
|
underline: textStyle.underline,
|
|
342
|
+
lang: 'zh-CN',
|
|
342
343
|
|
|
343
344
|
fill: fill,
|
|
344
345
|
align: align,
|
|
@@ -413,11 +414,11 @@ function getGradientFallbackColor(bgImage) {
|
|
|
413
414
|
// 3. Find first part that is a color (skip angle/direction)
|
|
414
415
|
for (const part of parts) {
|
|
415
416
|
// Ignore directions (to right) or angles (90deg, 0.5turn)
|
|
416
|
-
if (/^(to\s|[\d
|
|
417
|
+
if (/^(to\s|[\d.]+(deg|rad|turn|grad))/.test(part)) continue;
|
|
417
418
|
|
|
418
419
|
// Extract color: Remove trailing position (e.g. "red 50%" -> "red")
|
|
419
420
|
// Regex matches whitespace + number + unit at end of string
|
|
420
|
-
const colorPart = part.replace(/\s+(-?[\d
|
|
421
|
+
const colorPart = part.replace(/\s+(-?[\d.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
|
|
421
422
|
|
|
422
423
|
// Check if it's not just a number (some gradients might have bare numbers? unlikely in standard syntax)
|
|
423
424
|
if (colorPart) return colorPart;
|
|
@@ -688,6 +689,214 @@ function parseColor(str) {
|
|
|
688
689
|
return { hex, opacity: a };
|
|
689
690
|
}
|
|
690
691
|
|
|
692
|
+
const SUPPORTED_CHART_TYPES = new Set(['area', 'bar', 'line', 'pie', 'doughnut', 'radar', 'scatter']);
|
|
693
|
+
const CHART_TYPE_ALIASES = {
|
|
694
|
+
column: 'bar',
|
|
695
|
+
columnbar: 'bar',
|
|
696
|
+
column3d: 'bar',
|
|
697
|
+
bar3d: 'bar',
|
|
698
|
+
donut: 'doughnut',
|
|
699
|
+
ring: 'doughnut',
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
function normalizeLegendPos(value) {
|
|
703
|
+
if (!value) return null;
|
|
704
|
+
const normalized = value.toString().toLowerCase().replace(/[^a-z]/g, '');
|
|
705
|
+
if (normalized === 'tr' || normalized === 'topright') return 'tr';
|
|
706
|
+
if (normalized === 'top') return 't';
|
|
707
|
+
if (normalized === 'bottom') return 'b';
|
|
708
|
+
if (normalized === 'left') return 'l';
|
|
709
|
+
if (normalized === 'right') return 'r';
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function normalizeChartType(value) {
|
|
714
|
+
if (!value) return null;
|
|
715
|
+
const raw = value.toString().trim().toLowerCase();
|
|
716
|
+
const alias = CHART_TYPE_ALIASES[raw];
|
|
717
|
+
const candidate = alias || raw;
|
|
718
|
+
return SUPPORTED_CHART_TYPES.has(candidate) ? candidate : null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function colorToHex(value) {
|
|
722
|
+
const parsed = parseColor(value);
|
|
723
|
+
return parsed.hex || null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function resolveChartElement(root, config) {
|
|
727
|
+
if (!config) return null;
|
|
728
|
+
if (config.element instanceof HTMLElement) return config.element;
|
|
729
|
+
if (typeof config.element === 'function') return config.element(root);
|
|
730
|
+
if (typeof config.selector === 'string') {
|
|
731
|
+
return root.querySelector(config.selector);
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function buildSeriesFromSource(source = {}) {
|
|
737
|
+
const fallbackLabels = Array.isArray(source.labels) ? source.labels : [];
|
|
738
|
+
const candidateDatasets = Array.isArray(source.datasets) ? source.datasets : [];
|
|
739
|
+
const series = [];
|
|
740
|
+
|
|
741
|
+
candidateDatasets.forEach((dataset, index) => {
|
|
742
|
+
if (!dataset) return;
|
|
743
|
+
const values = Array.isArray(dataset.values)
|
|
744
|
+
? dataset.values
|
|
745
|
+
: Array.isArray(dataset.data)
|
|
746
|
+
? dataset.data
|
|
747
|
+
: [];
|
|
748
|
+
|
|
749
|
+
if (!values.length) return;
|
|
750
|
+
|
|
751
|
+
const record = {
|
|
752
|
+
name: dataset.name || dataset.label || `Series ${index + 1}`,
|
|
753
|
+
values,
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
if (Array.isArray(dataset.labels) && dataset.labels.length === values.length) {
|
|
757
|
+
record.labels = dataset.labels;
|
|
758
|
+
} else if (fallbackLabels.length === values.length) {
|
|
759
|
+
record.labels = fallbackLabels;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (Array.isArray(dataset.sizes) && dataset.sizes.length === values.length) {
|
|
763
|
+
record.sizes = dataset.sizes;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Chart.js often provides strings/arrays in backgroundColor
|
|
767
|
+
const candidateColor =
|
|
768
|
+
dataset.color || dataset.backgroundColor || dataset.borderColor || dataset.fillColor;
|
|
769
|
+
if (candidateColor) {
|
|
770
|
+
if (Array.isArray(candidateColor)) {
|
|
771
|
+
record.color = candidateColor[0];
|
|
772
|
+
} else {
|
|
773
|
+
record.color = candidateColor;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
series.push(record);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (!series.length) {
|
|
781
|
+
const fallbackValues = Array.isArray(source.values)
|
|
782
|
+
? source.values
|
|
783
|
+
: Array.isArray(source.data)
|
|
784
|
+
? source.data
|
|
785
|
+
: [];
|
|
786
|
+
|
|
787
|
+
if (fallbackValues.length) {
|
|
788
|
+
const fallbackRecord = {
|
|
789
|
+
name: source.name || source.label || 'Series 1',
|
|
790
|
+
values: fallbackValues,
|
|
791
|
+
};
|
|
792
|
+
if (Array.isArray(source.labels) && source.labels.length === fallbackValues.length) {
|
|
793
|
+
fallbackRecord.labels = source.labels;
|
|
794
|
+
} else if (fallbackLabels.length === fallbackValues.length) {
|
|
795
|
+
fallbackRecord.labels = fallbackLabels;
|
|
796
|
+
}
|
|
797
|
+
if (Array.isArray(source.sizes) && source.sizes.length === fallbackValues.length) {
|
|
798
|
+
fallbackRecord.sizes = source.sizes;
|
|
799
|
+
}
|
|
800
|
+
if (source.color) {
|
|
801
|
+
fallbackRecord.color = source.color;
|
|
802
|
+
}
|
|
803
|
+
series.push(fallbackRecord);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return series;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function buildChartOptions(raw, derivedColors) {
|
|
811
|
+
const opts = { ...((raw && raw.chartOptions) || {}) };
|
|
812
|
+
if (raw && raw.showLegend !== undefined) opts.showLegend = raw.showLegend;
|
|
813
|
+
if (raw) {
|
|
814
|
+
const legendTarget = raw.legendPos || raw.legendPosition;
|
|
815
|
+
const normalizedLegend = normalizeLegendPos(legendTarget);
|
|
816
|
+
if (normalizedLegend) opts.legendPos = normalizedLegend;
|
|
817
|
+
}
|
|
818
|
+
if (raw && raw.title) opts.title = raw.title;
|
|
819
|
+
if (raw && raw.altText) opts.altText = raw.altText;
|
|
820
|
+
if (raw && raw.showDataTable !== undefined) opts.showDataTable = raw.showDataTable;
|
|
821
|
+
if (raw && raw.chartColorsOpacity !== undefined) opts.chartColorsOpacity = raw.chartColorsOpacity;
|
|
822
|
+
if (raw && raw.showLabel !== undefined) opts.showLabel = raw.showLabel;
|
|
823
|
+
|
|
824
|
+
const paletteFromConfig =
|
|
825
|
+
raw && Array.isArray(raw.chartColors) ? raw.chartColors.map(colorToHex).filter(Boolean) : [];
|
|
826
|
+
const palette = paletteFromConfig.length ? paletteFromConfig : derivedColors;
|
|
827
|
+
if (palette.length && !opts.chartColors) {
|
|
828
|
+
opts.chartColors = palette;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return opts;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function normalizeChartConfig(raw) {
|
|
835
|
+
if (!raw) return null;
|
|
836
|
+
const chartType = normalizeChartType(raw.chartType || raw.type || raw.chart);
|
|
837
|
+
if (!chartType) return null;
|
|
838
|
+
|
|
839
|
+
let seriesWithColor = [];
|
|
840
|
+
if (Array.isArray(raw.chartData) && raw.chartData.length) {
|
|
841
|
+
seriesWithColor = raw.chartData
|
|
842
|
+
.map((entry, index) => {
|
|
843
|
+
if (!entry || !Array.isArray(entry.values)) return null;
|
|
844
|
+
return {
|
|
845
|
+
...entry,
|
|
846
|
+
name: entry.name || entry.label || `Series ${index + 1}`,
|
|
847
|
+
};
|
|
848
|
+
})
|
|
849
|
+
.filter((entry) => entry && entry.values && entry.values.length);
|
|
850
|
+
} else {
|
|
851
|
+
const source = {
|
|
852
|
+
labels: raw.data?.labels || raw.labels,
|
|
853
|
+
datasets: Array.isArray(raw.data?.datasets)
|
|
854
|
+
? raw.data.datasets
|
|
855
|
+
: Array.isArray(raw.datasets)
|
|
856
|
+
? raw.datasets
|
|
857
|
+
: [],
|
|
858
|
+
values: raw.data?.values || raw.values,
|
|
859
|
+
data: raw.data?.data,
|
|
860
|
+
name: raw.name,
|
|
861
|
+
label: raw.label,
|
|
862
|
+
color: raw.color,
|
|
863
|
+
sizes: raw.data?.sizes || raw.sizes,
|
|
864
|
+
};
|
|
865
|
+
seriesWithColor = buildSeriesFromSource(source);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (!seriesWithColor.length) return null;
|
|
869
|
+
|
|
870
|
+
const derivedColors = seriesWithColor
|
|
871
|
+
.map((dataset) => dataset.color)
|
|
872
|
+
.map(colorToHex)
|
|
873
|
+
.filter(Boolean);
|
|
874
|
+
|
|
875
|
+
const chartOptions = buildChartOptions(raw, derivedColors);
|
|
876
|
+
const chartData = seriesWithColor.map(({ color, ...rest }) => rest);
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
chartType,
|
|
880
|
+
chartData,
|
|
881
|
+
chartOptions,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function buildChartRegistry(root, chartConfigs = []) {
|
|
886
|
+
const registry = new Map();
|
|
887
|
+
if (!root || !Array.isArray(chartConfigs)) return registry;
|
|
888
|
+
|
|
889
|
+
chartConfigs.forEach((config) => {
|
|
890
|
+
const node = resolveChartElement(root, config);
|
|
891
|
+
if (!node) return;
|
|
892
|
+
const normalized = normalizeChartConfig(config);
|
|
893
|
+
if (!normalized || !normalized.chartData || !normalized.chartData.length) return;
|
|
894
|
+
registry.set(node, normalized);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
return registry;
|
|
898
|
+
}
|
|
899
|
+
|
|
691
900
|
function getPadding(style, scale) {
|
|
692
901
|
const pxToInch = 1 / 96;
|
|
693
902
|
return [
|
|
@@ -706,6 +915,8 @@ function getSoftEdges(filterStr, scale) {
|
|
|
706
915
|
}
|
|
707
916
|
|
|
708
917
|
const DEFAULT_CJK_FONTS = [
|
|
918
|
+
'Heiti TC',
|
|
919
|
+
'Heiti SC',
|
|
709
920
|
'PingFang SC',
|
|
710
921
|
'Hiragino Sans GB',
|
|
711
922
|
'Microsoft YaHei',
|
|
@@ -757,7 +968,7 @@ function pickFontFace(fontFamily, text, options) {
|
|
|
757
968
|
}
|
|
758
969
|
|
|
759
970
|
const autoMatch = DEFAULT_CJK_FONTS.find((f) => lowered.includes(f.toLowerCase()));
|
|
760
|
-
return autoMatch ||
|
|
971
|
+
return autoMatch || 'Heiti TC';
|
|
761
972
|
}
|
|
762
973
|
|
|
763
974
|
function getTextStyle(style, scale, text = '', options = {}) {
|
|
@@ -788,6 +999,9 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
788
999
|
// And apply the global layout scale.
|
|
789
1000
|
lineSpacing = lhPx * 0.75 * scale;
|
|
790
1001
|
}
|
|
1002
|
+
} else {
|
|
1003
|
+
// Default line height when 'normal' - use 1.2 multiplier to match browser default
|
|
1004
|
+
lineSpacing = fontSizePx * 1.2 * 0.75 * scale;
|
|
791
1005
|
}
|
|
792
1006
|
|
|
793
1007
|
// --- Spacing (Margins) ---
|
|
@@ -806,10 +1020,11 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
806
1020
|
return {
|
|
807
1021
|
color: colorObj.hex || '000000',
|
|
808
1022
|
fontFace: fontFace,
|
|
809
|
-
fontSize: Math.floor(fontSizePx * 0.75 * scale),
|
|
1023
|
+
fontSize: Math.max(8, Math.floor(fontSizePx * 0.75 * scale)),
|
|
810
1024
|
bold: parseInt(style.fontWeight) >= 600,
|
|
811
1025
|
italic: style.fontStyle === 'italic',
|
|
812
1026
|
underline: style.textDecoration.includes('underline'),
|
|
1027
|
+
lang: 'zh-CN',
|
|
813
1028
|
// Only add if we have a valid value
|
|
814
1029
|
...(lineSpacing && { lineSpacing }),
|
|
815
1030
|
...(paraSpaceBefore > 0 && { paraSpaceBefore }),
|
|
@@ -822,6 +1037,7 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
822
1037
|
/**
|
|
823
1038
|
* Determines if a given DOM node is primarily a text container.
|
|
824
1039
|
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
1040
|
+
* Also rejects flex/grid containers with distributed children (space-between/around/evenly).
|
|
825
1041
|
*/
|
|
826
1042
|
function isTextContainer(node) {
|
|
827
1043
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -830,6 +1046,26 @@ function isTextContainer(node) {
|
|
|
830
1046
|
const children = Array.from(node.children);
|
|
831
1047
|
if (children.length === 0) return true;
|
|
832
1048
|
|
|
1049
|
+
// Check if parent is a flex/grid container with special layout
|
|
1050
|
+
// In such cases, children should be treated as separate elements
|
|
1051
|
+
const parentStyle = window.getComputedStyle(node);
|
|
1052
|
+
const display = parentStyle.display;
|
|
1053
|
+
const justifyContent = parentStyle.justifyContent || '';
|
|
1054
|
+
parentStyle.alignItems || '';
|
|
1055
|
+
|
|
1056
|
+
// If parent uses flex/grid with space distribution, don't treat as text container
|
|
1057
|
+
if ((display.includes('flex') || display.includes('grid')) &&
|
|
1058
|
+
(justifyContent.includes('space-between') ||
|
|
1059
|
+
justifyContent.includes('space-around') ||
|
|
1060
|
+
justifyContent.includes('space-evenly'))) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Note: We don't skip text container for align-items: center here.
|
|
1065
|
+
// The valign inheritance is handled in prepareRenderItem with single-child check.
|
|
1066
|
+
// This allows elements like "密钥状态流转" to properly inherit center alignment
|
|
1067
|
+
// while preventing STEP 1/2/3 from being incorrectly centered.
|
|
1068
|
+
|
|
833
1069
|
const isSafeInline = (el) => {
|
|
834
1070
|
// 1. Reject Web Components / Custom Elements
|
|
835
1071
|
if (el.tagName.includes('-')) return false;
|
|
@@ -1388,6 +1624,60 @@ const PptxGenJS = PptxGenJSImport__namespace?.default ?? PptxGenJSImport__namesp
|
|
|
1388
1624
|
const PPI = 96;
|
|
1389
1625
|
const PX_TO_INCH = 1 / PPI;
|
|
1390
1626
|
|
|
1627
|
+
/**
|
|
1628
|
+
* Fix Chinese font attributes (pitchFamily and charset) in PPTX XML.
|
|
1629
|
+
* PptxGenJS sometimes generates incorrect charset="0" for East Asian fonts.
|
|
1630
|
+
* @param {Blob} blob - The PPTX blob
|
|
1631
|
+
* @returns {Promise<Blob>} - Fixed PPTX blob
|
|
1632
|
+
*/
|
|
1633
|
+
async function fixChineseFontAttributes(blob) {
|
|
1634
|
+
const zip = await JSZip__default["default"].loadAsync(blob);
|
|
1635
|
+
const cjkFonts = ['Heiti TC', 'Heiti SC', 'PingFang SC', 'Microsoft YaHei', 'SimHei', 'SimSun'];
|
|
1636
|
+
|
|
1637
|
+
// Process all slide XML files
|
|
1638
|
+
const slideFiles = Object.keys(zip.files).filter(name =>
|
|
1639
|
+
name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
for (const slidePath of slideFiles) {
|
|
1643
|
+
let xml = await zip.file(slidePath).async('text');
|
|
1644
|
+
let modified = false;
|
|
1645
|
+
|
|
1646
|
+
// Fix <a:ea> and <a:cs> tags for CJK fonts
|
|
1647
|
+
// Replace tags that have charset="0" (incorrect for CJK) with proper values
|
|
1648
|
+
for (const fontName of cjkFonts) {
|
|
1649
|
+
// Fix <a:ea> - East Asian font
|
|
1650
|
+
const eaPattern = new RegExp(
|
|
1651
|
+
`<a:ea typeface="${fontName}"[^>]*>`,
|
|
1652
|
+
'g'
|
|
1653
|
+
);
|
|
1654
|
+
xml = xml.replace(eaPattern, `<a:ea typeface="${fontName}" pitchFamily="34" charset="-122"/>`);
|
|
1655
|
+
|
|
1656
|
+
// Fix <a:cs> - Complex Script font
|
|
1657
|
+
const csPattern = new RegExp(
|
|
1658
|
+
`<a:cs typeface="${fontName}"[^>]*>`,
|
|
1659
|
+
'g'
|
|
1660
|
+
);
|
|
1661
|
+
xml = xml.replace(csPattern, `<a:cs typeface="${fontName}" pitchFamily="34" charset="-120"/>`);
|
|
1662
|
+
|
|
1663
|
+
// Fix <a:latin> - Latin font (if it's a CJK font used as Latin)
|
|
1664
|
+
const latinPattern = new RegExp(
|
|
1665
|
+
`<a:latin typeface="${fontName}"[^>]*>`,
|
|
1666
|
+
'g'
|
|
1667
|
+
);
|
|
1668
|
+
xml = xml.replace(latinPattern, `<a:latin typeface="${fontName}" pitchFamily="34" charset="0"/>`);
|
|
1669
|
+
}
|
|
1670
|
+
modified = true;
|
|
1671
|
+
|
|
1672
|
+
if (modified) {
|
|
1673
|
+
zip.file(slidePath, xml);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Generate new blob
|
|
1678
|
+
return await zip.generateAsync({ type: 'blob' });
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1391
1681
|
/**
|
|
1392
1682
|
* Main export function.
|
|
1393
1683
|
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
@@ -1412,6 +1702,7 @@ async function exportToPptx(target, options = {}) {
|
|
|
1412
1702
|
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
1413
1703
|
const pptx = new PptxConstructor();
|
|
1414
1704
|
pptx.layout = 'LAYOUT_16x9';
|
|
1705
|
+
pptx.language = 'zh-CN';
|
|
1415
1706
|
|
|
1416
1707
|
const elements = Array.isArray(target) ? target : [target];
|
|
1417
1708
|
|
|
@@ -1486,6 +1777,9 @@ async function exportToPptx(target, options = {}) {
|
|
|
1486
1777
|
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
1487
1778
|
}
|
|
1488
1779
|
|
|
1780
|
+
// Fix Chinese font attributes (pitchFamily and charset) in all slide XMLs
|
|
1781
|
+
finalBlob = await fixChineseFontAttributes(finalBlob);
|
|
1782
|
+
|
|
1489
1783
|
// 4. Output Handling
|
|
1490
1784
|
// If skipDownload is NOT true, proceed with browser download
|
|
1491
1785
|
if (!options.skipDownload) {
|
|
@@ -1527,6 +1821,9 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1527
1821
|
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
1528
1822
|
};
|
|
1529
1823
|
|
|
1824
|
+
const chartRegistry = buildChartRegistry(root, globalOptions.chartConfigs);
|
|
1825
|
+
const slideOptions = { ...globalOptions, chartRegistry };
|
|
1826
|
+
|
|
1530
1827
|
const renderQueue = [];
|
|
1531
1828
|
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
1532
1829
|
let domOrderCounter = 0;
|
|
@@ -1562,7 +1859,7 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1562
1859
|
pptx,
|
|
1563
1860
|
currentZ,
|
|
1564
1861
|
nodeStyle,
|
|
1565
|
-
|
|
1862
|
+
slideOptions
|
|
1566
1863
|
);
|
|
1567
1864
|
|
|
1568
1865
|
if (result) {
|
|
@@ -1608,6 +1905,10 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1608
1905
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
1609
1906
|
if (item.type === 'image') slide.addImage(item.options);
|
|
1610
1907
|
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
1908
|
+
if (item.type === 'chart') {
|
|
1909
|
+
const chartType = PptxGenJS.ChartType?.[item.chartType] ?? item.chartType;
|
|
1910
|
+
slide.addChart(chartType, item.chartData, item.options);
|
|
1911
|
+
}
|
|
1611
1912
|
if (item.type === 'table') {
|
|
1612
1913
|
slide.addTable(item.tableData.rows, {
|
|
1613
1914
|
x: item.options.x,
|
|
@@ -1808,13 +2109,23 @@ function prepareRenderItem(
|
|
|
1808
2109
|
range.detach();
|
|
1809
2110
|
|
|
1810
2111
|
const style = window.getComputedStyle(parent);
|
|
1811
|
-
|
|
1812
|
-
|
|
2112
|
+
// Use parent element's rect for better width calculation
|
|
2113
|
+
// This is especially important for flex children where text node rect may be too narrow
|
|
2114
|
+
const parentRect = parent.getBoundingClientRect();
|
|
2115
|
+
const useParentRect = parentRect.width > rect.width;
|
|
2116
|
+
let widthPx = useParentRect ? parentRect.width : rect.width;
|
|
2117
|
+
const heightPx = useParentRect ? parentRect.height : rect.height;
|
|
2118
|
+
|
|
2119
|
+
// Add extra width buffer to prevent text wrapping in PPTX due to font rendering differences
|
|
2120
|
+
// Chinese characters and certain fonts need more space in PPTX than in browser
|
|
2121
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
1813
2122
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
1814
2123
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
1815
2124
|
|
|
1816
|
-
|
|
1817
|
-
const
|
|
2125
|
+
// Use parent rect's position if we're using parent dimensions
|
|
2126
|
+
const sourceRect = useParentRect ? parentRect : rect;
|
|
2127
|
+
const x = config.offX + (sourceRect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
2128
|
+
const y = config.offY + (sourceRect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
1818
2129
|
|
|
1819
2130
|
return {
|
|
1820
2131
|
items: [
|
|
@@ -1853,7 +2164,7 @@ function prepareRenderItem(
|
|
|
1853
2164
|
const elementOpacity = parseFloat(style.opacity);
|
|
1854
2165
|
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
1855
2166
|
|
|
1856
|
-
|
|
2167
|
+
let widthPx = node.offsetWidth || rect.width;
|
|
1857
2168
|
const heightPx = node.offsetHeight || rect.height;
|
|
1858
2169
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
1859
2170
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
@@ -1867,8 +2178,41 @@ function prepareRenderItem(
|
|
|
1867
2178
|
|
|
1868
2179
|
const items = [];
|
|
1869
2180
|
|
|
1870
|
-
|
|
1871
|
-
|
|
2181
|
+
const chartConfig = globalOptions.chartRegistry?.get(node);
|
|
2182
|
+
if (chartConfig) {
|
|
2183
|
+
const chartOptions = {
|
|
2184
|
+
...chartConfig.chartOptions,
|
|
2185
|
+
x,
|
|
2186
|
+
y,
|
|
2187
|
+
w: unrotatedW,
|
|
2188
|
+
h: unrotatedH,
|
|
2189
|
+
};
|
|
2190
|
+
|
|
2191
|
+
return {
|
|
2192
|
+
items: [
|
|
2193
|
+
{
|
|
2194
|
+
type: 'chart',
|
|
2195
|
+
zIndex: effectiveZIndex,
|
|
2196
|
+
domOrder,
|
|
2197
|
+
chartType: chartConfig.chartType,
|
|
2198
|
+
chartData: chartConfig.chartData,
|
|
2199
|
+
options: chartOptions,
|
|
2200
|
+
},
|
|
2201
|
+
],
|
|
2202
|
+
stopRecursion: true,
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// --- Handle both native TABLE and div-based table structures ---
|
|
2207
|
+
const isTableLike = node.tagName === 'TABLE' || detectTableLikeStructure(node);
|
|
2208
|
+
if (isTableLike) {
|
|
2209
|
+
let tableData;
|
|
2210
|
+
if (node.tagName === 'TABLE') {
|
|
2211
|
+
tableData = extractTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
2212
|
+
} else {
|
|
2213
|
+
// Extract data from div-based table structure
|
|
2214
|
+
tableData = extractDivTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
2215
|
+
}
|
|
1872
2216
|
const cellBgItems = [];
|
|
1873
2217
|
const renderCellBg = globalOptions.tableConfig?.renderCellBackgrounds !== false;
|
|
1874
2218
|
|
|
@@ -2212,6 +2556,35 @@ function prepareRenderItem(
|
|
|
2212
2556
|
return { items: [item], job, stopRecursion: true };
|
|
2213
2557
|
}
|
|
2214
2558
|
|
|
2559
|
+
// --- Handle vertical stat cards (like .mini-stat with number + label) ---
|
|
2560
|
+
if (isVerticalStatCard(node)) {
|
|
2561
|
+
// Capture the entire stat card as an image to preserve vertical layout
|
|
2562
|
+
const item = {
|
|
2563
|
+
type: 'image',
|
|
2564
|
+
zIndex,
|
|
2565
|
+
domOrder,
|
|
2566
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
2567
|
+
};
|
|
2568
|
+
const job = async () => {
|
|
2569
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
2570
|
+
if (pngData) item.options.data = pngData;
|
|
2571
|
+
else item.skip = true;
|
|
2572
|
+
};
|
|
2573
|
+
return { items: [item], job, stopRecursion: true };
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// --- Handle absolutely positioned children that overflow parent ---
|
|
2577
|
+
// Check if this element has absolutely positioned children that extend outside
|
|
2578
|
+
const overflowingChildren = detectOverflowingChildren(node);
|
|
2579
|
+
if (overflowingChildren.length > 0) {
|
|
2580
|
+
// Process this node normally, but also capture overflowing children separately
|
|
2581
|
+
const baseResult = processOverflowingContent(
|
|
2582
|
+
node, overflowingChildren, config, domOrder, pptx, zIndex);
|
|
2583
|
+
if (baseResult) {
|
|
2584
|
+
return baseResult;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2215
2588
|
// Radii logic
|
|
2216
2589
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
2217
2590
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
@@ -2314,6 +2687,27 @@ function prepareRenderItem(
|
|
|
2314
2687
|
let textPayload = null;
|
|
2315
2688
|
const isText = isTextContainer(node);
|
|
2316
2689
|
|
|
2690
|
+
// Add extra width buffer for inline text elements to prevent wrapping in PPTX
|
|
2691
|
+
// This is especially needed for Chinese text and certain fonts
|
|
2692
|
+
// Only apply to elements that are direct children of flex containers with space distribution
|
|
2693
|
+
// and don't have a parent with visible background (to avoid overflowing card boundaries)
|
|
2694
|
+
if (isText && !bgColorObj.hex && !hasBorder && !hasShadow) {
|
|
2695
|
+
const parentEl = node.parentElement;
|
|
2696
|
+
if (parentEl) {
|
|
2697
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
2698
|
+
const parentDisplay = parentStyle.display || '';
|
|
2699
|
+
const parentJustify = parentStyle.justifyContent || '';
|
|
2700
|
+
const isFlexWithSpaceDist = (parentDisplay.includes('flex') || parentDisplay.includes('grid')) &&
|
|
2701
|
+
(parentJustify.includes('space-between') || parentJustify.includes('space-around') || parentJustify.includes('space-evenly'));
|
|
2702
|
+
|
|
2703
|
+
if (isFlexWithSpaceDist) {
|
|
2704
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
2705
|
+
// Recalculate w with the new width
|
|
2706
|
+
w = widthPx * PX_TO_INCH * config.scale;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2317
2711
|
if (isText) {
|
|
2318
2712
|
const textParts = [];
|
|
2319
2713
|
let trimNextLeading = false;
|
|
@@ -2370,8 +2764,71 @@ function prepareRenderItem(
|
|
|
2370
2764
|
let align = style.textAlign || 'left';
|
|
2371
2765
|
if (align === 'start') align = 'left';
|
|
2372
2766
|
if (align === 'end') align = 'right';
|
|
2767
|
+
|
|
2768
|
+
// Fix: If this element is a flex/grid child and has no explicit text-align,
|
|
2769
|
+
// force left alignment to match HTML default behavior
|
|
2770
|
+
if (node.parentElement && (!style.textAlign || style.textAlign === 'start')) {
|
|
2771
|
+
const parentStyle = window.getComputedStyle(node.parentElement);
|
|
2772
|
+
if (parentStyle.display.includes('flex') || parentStyle.display.includes('grid')) {
|
|
2773
|
+
align = 'left';
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// Detect badge/pill buttons (high border-radius + short text) and auto-center them
|
|
2778
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
2779
|
+
const height = parseFloat(style.height) || node.offsetHeight;
|
|
2780
|
+
const textContent = node.textContent.trim();
|
|
2781
|
+
const className = (node.className || '').toLowerCase();
|
|
2782
|
+
|
|
2783
|
+
// Real badges/pills typically have visible styling (background, border, or large border-radius)
|
|
2784
|
+
const hasVisibleBackground = bgColorObj.hex && bgColorObj.opacity > 0.1;
|
|
2785
|
+
const hasVisibleBorder = borderWidth > 0;
|
|
2786
|
+
const hasLargeBorderRadius = borderRadius >= height / 2;
|
|
2787
|
+
const hasBadgeClass = className.includes('badge') || className.includes('pill');
|
|
2788
|
+
|
|
2789
|
+
// Only consider it a badge if it has visual styling AND short text
|
|
2790
|
+
const isLikelyBadge =
|
|
2791
|
+
(hasLargeBorderRadius || hasBadgeClass) &&
|
|
2792
|
+
textContent.length <= 10 &&
|
|
2793
|
+
(hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius);
|
|
2794
|
+
|
|
2795
|
+
if (isLikelyBadge) {
|
|
2796
|
+
align = 'center';
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2373
2799
|
let valign = 'top';
|
|
2374
|
-
|
|
2800
|
+
|
|
2801
|
+
// For flex items, valign should be determined by PARENT's align-items, not self
|
|
2802
|
+
// Self's align-items controls how children are aligned, not self's position in parent
|
|
2803
|
+
const parentEl = node.parentElement;
|
|
2804
|
+
if (parentEl) {
|
|
2805
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
2806
|
+
if (parentStyle.display.includes('flex')) {
|
|
2807
|
+
// Parent's align-items controls this element's cross-axis alignment
|
|
2808
|
+
if (parentStyle.alignItems === 'center') {
|
|
2809
|
+
valign = 'middle';
|
|
2810
|
+
} else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
|
|
2811
|
+
valign = 'bottom';
|
|
2812
|
+
}
|
|
2813
|
+
// Default (stretch, flex-start) keeps valign = 'top'
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// If element is not a flex item (no flex parent), then its own align-items
|
|
2818
|
+
// might indicate self-centering intent for single-element containers
|
|
2819
|
+
if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
|
|
2820
|
+
valign = 'middle';
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// Auto-center vertically for likely badges
|
|
2824
|
+
if (isLikelyBadge && valign !== 'middle') {
|
|
2825
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
2826
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
2827
|
+
// If padding is relatively even, assume vertical centering is intended
|
|
2828
|
+
if (Math.abs(paddingTop - paddingBottom) <= 4) {
|
|
2829
|
+
valign = 'middle';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2375
2832
|
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
2376
2833
|
|
|
2377
2834
|
const pt = parseFloat(style.paddingTop) || 0;
|
|
@@ -2731,5 +3188,282 @@ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder)
|
|
|
2731
3188
|
return items;
|
|
2732
3189
|
}
|
|
2733
3190
|
|
|
3191
|
+
/**
|
|
3192
|
+
* Detect absolutely positioned children that overflow their parent
|
|
3193
|
+
* This handles cases like chart value labels positioned above bars
|
|
3194
|
+
*/
|
|
3195
|
+
function detectOverflowingChildren(node) {
|
|
3196
|
+
const overflowing = [];
|
|
3197
|
+
const parentRect = node.getBoundingClientRect();
|
|
3198
|
+
|
|
3199
|
+
// Only check for elements with specific class patterns that suggest chart values
|
|
3200
|
+
const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
|
|
3201
|
+
|
|
3202
|
+
children.forEach(child => {
|
|
3203
|
+
const childStyle = window.getComputedStyle(child);
|
|
3204
|
+
const childRect = child.getBoundingClientRect();
|
|
3205
|
+
|
|
3206
|
+
// Check if absolutely positioned and outside parent bounds
|
|
3207
|
+
if (childStyle.position === 'absolute') {
|
|
3208
|
+
const isOutside =
|
|
3209
|
+
childRect.bottom < parentRect.top ||
|
|
3210
|
+
childRect.top > parentRect.bottom ||
|
|
3211
|
+
childRect.right < parentRect.left ||
|
|
3212
|
+
childRect.left > parentRect.right;
|
|
3213
|
+
|
|
3214
|
+
if (isOutside || childRect.top < parentRect.top) {
|
|
3215
|
+
overflowing.push(child);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
|
|
3220
|
+
return overflowing;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/**
|
|
3224
|
+
* Process content with overflowing children
|
|
3225
|
+
* Captures the entire visual area including overflowing elements
|
|
3226
|
+
*/
|
|
3227
|
+
function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
|
|
3228
|
+
// Calculate bounding box that includes all overflowing children
|
|
3229
|
+
const rect = node.getBoundingClientRect();
|
|
3230
|
+
let minX = rect.left;
|
|
3231
|
+
let minY = rect.top;
|
|
3232
|
+
let maxX = rect.right;
|
|
3233
|
+
let maxY = rect.bottom;
|
|
3234
|
+
|
|
3235
|
+
overflowingChildren.forEach(child => {
|
|
3236
|
+
const childRect = child.getBoundingClientRect();
|
|
3237
|
+
minX = Math.min(minX, childRect.left);
|
|
3238
|
+
minY = Math.min(minY, childRect.top);
|
|
3239
|
+
maxX = Math.max(maxX, childRect.right);
|
|
3240
|
+
maxY = Math.max(maxY, childRect.bottom);
|
|
3241
|
+
});
|
|
3242
|
+
|
|
3243
|
+
const totalWidth = maxX - minX;
|
|
3244
|
+
const totalHeight = maxY - minY;
|
|
3245
|
+
|
|
3246
|
+
// Use html2canvas to capture the entire area
|
|
3247
|
+
const item = {
|
|
3248
|
+
type: 'image',
|
|
3249
|
+
zIndex,
|
|
3250
|
+
domOrder,
|
|
3251
|
+
options: {
|
|
3252
|
+
x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
|
|
3253
|
+
y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
|
|
3254
|
+
w: totalWidth * PX_TO_INCH * config.scale,
|
|
3255
|
+
h: totalHeight * PX_TO_INCH * config.scale,
|
|
3256
|
+
data: null
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
3259
|
+
|
|
3260
|
+
const job = async () => {
|
|
3261
|
+
try {
|
|
3262
|
+
// Temporarily adjust node position for capture
|
|
3263
|
+
const originalPosition = node.style.position;
|
|
3264
|
+
const originalTransform = node.style.transform;
|
|
3265
|
+
|
|
3266
|
+
node.style.position = 'relative';
|
|
3267
|
+
node.style.transform = 'none';
|
|
3268
|
+
|
|
3269
|
+
const canvas = await html2canvas__default["default"](node, {
|
|
3270
|
+
backgroundColor: null,
|
|
3271
|
+
logging: false,
|
|
3272
|
+
scale: 2,
|
|
3273
|
+
useCORS: true,
|
|
3274
|
+
x: minX - rect.left,
|
|
3275
|
+
y: minY - rect.top,
|
|
3276
|
+
width: totalWidth,
|
|
3277
|
+
height: totalHeight
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
// Restore original styles
|
|
3281
|
+
node.style.position = originalPosition;
|
|
3282
|
+
node.style.transform = originalTransform;
|
|
3283
|
+
|
|
3284
|
+
item.options.data = canvas.toDataURL('image/png');
|
|
3285
|
+
} catch (e) {
|
|
3286
|
+
console.warn('Failed to capture overflowing content:', e);
|
|
3287
|
+
item.skip = true;
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
|
|
3291
|
+
return { items: [item], job, stopRecursion: true };
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
/**
|
|
3295
|
+
* Detect vertical stat cards (like .mini-stat with number above label)
|
|
3296
|
+
* These have block-level children that should stack vertically
|
|
3297
|
+
*/
|
|
3298
|
+
function isVerticalStatCard(node) {
|
|
3299
|
+
const className = (node.className || '').toLowerCase();
|
|
3300
|
+
|
|
3301
|
+
// Check for stat-like class names
|
|
3302
|
+
const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
|
|
3303
|
+
const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
|
|
3304
|
+
|
|
3305
|
+
if (!hasStatClass) return false;
|
|
3306
|
+
|
|
3307
|
+
const children = Array.from(node.children);
|
|
3308
|
+
if (children.length !== 2) return false;
|
|
3309
|
+
|
|
3310
|
+
// Check if children are likely number + label pair
|
|
3311
|
+
const child1 = children[0];
|
|
3312
|
+
const child2 = children[1];
|
|
3313
|
+
|
|
3314
|
+
const style1 = window.getComputedStyle(child1);
|
|
3315
|
+
const style2 = window.getComputedStyle(child2);
|
|
3316
|
+
|
|
3317
|
+
// Both should be block elements (or have block-like display)
|
|
3318
|
+
const isBlock1 = style1.display === 'block' || style1.display === 'flex';
|
|
3319
|
+
const isBlock2 = style2.display === 'block' || style2.display === 'flex';
|
|
3320
|
+
|
|
3321
|
+
if (!isBlock1 || !isBlock2) return false;
|
|
3322
|
+
|
|
3323
|
+
// First child should have larger font (the number)
|
|
3324
|
+
const fontSize1 = parseFloat(style1.fontSize) || 0;
|
|
3325
|
+
const fontSize2 = parseFloat(style2.fontSize) || 0;
|
|
3326
|
+
|
|
3327
|
+
// Number should be larger than label, or at least bold
|
|
3328
|
+
const isBold1 = parseInt(style1.fontWeight) >= 600;
|
|
3329
|
+
|
|
3330
|
+
return fontSize1 >= fontSize2 || isBold1;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
/**
|
|
3334
|
+
* Detect if a div structure looks like a table
|
|
3335
|
+
* Checks for table-like class names and structure patterns
|
|
3336
|
+
*/
|
|
3337
|
+
function detectTableLikeStructure(node) {
|
|
3338
|
+
const className = (node.className || '').toLowerCase();
|
|
3339
|
+
|
|
3340
|
+
// Check for table-related class names
|
|
3341
|
+
const tableIndicators = ['table', 'data-table', 'grid', 'list'];
|
|
3342
|
+
const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
|
|
3343
|
+
|
|
3344
|
+
if (!hasTableClass) return false;
|
|
3345
|
+
|
|
3346
|
+
// Check structure: should have header row and data rows
|
|
3347
|
+
const children = Array.from(node.children);
|
|
3348
|
+
|
|
3349
|
+
// Look for header-like element
|
|
3350
|
+
const hasHeader = children.some(child =>
|
|
3351
|
+
(child.className || '').toLowerCase().includes('header') ||
|
|
3352
|
+
child.tagName === 'THEAD'
|
|
3353
|
+
);
|
|
3354
|
+
|
|
3355
|
+
// Look for row-like elements
|
|
3356
|
+
const hasRows = children.some(child =>
|
|
3357
|
+
(child.className || '').toLowerCase().includes('row') ||
|
|
3358
|
+
child.tagName === 'TR' ||
|
|
3359
|
+
child.tagName === 'TBODY'
|
|
3360
|
+
);
|
|
3361
|
+
|
|
3362
|
+
// Check for cell-like structure in children
|
|
3363
|
+
const hasCellStructure = children.some(child => {
|
|
3364
|
+
const childChildren = Array.from(child.children);
|
|
3365
|
+
return childChildren.some(grandchild =>
|
|
3366
|
+
(grandchild.className || '').toLowerCase().includes('cell') ||
|
|
3367
|
+
grandchild.className.toLowerCase().includes('col') ||
|
|
3368
|
+
grandchild.className.toLowerCase().includes('td')
|
|
3369
|
+
);
|
|
3370
|
+
});
|
|
3371
|
+
|
|
3372
|
+
return (hasHeader || hasRows) && hasCellStructure;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* Extract table data from div-based table structure
|
|
3377
|
+
* Similar to extractTableData but for div grids
|
|
3378
|
+
*/
|
|
3379
|
+
function extractDivTableData(node, scale, options = {}) {
|
|
3380
|
+
const rows = [];
|
|
3381
|
+
const colWidths = [];
|
|
3382
|
+
const root = options.root || null;
|
|
3383
|
+
|
|
3384
|
+
// Find header row
|
|
3385
|
+
const headerRow = node.querySelector('.table-header, [class*="header"]');
|
|
3386
|
+
if (headerRow) {
|
|
3387
|
+
const headerCells = Array.from(headerRow.children);
|
|
3388
|
+
headerCells.forEach((cell, index) => {
|
|
3389
|
+
const rect = cell.getBoundingClientRect();
|
|
3390
|
+
const wIn = rect.width * (1 / 96) * scale;
|
|
3391
|
+
colWidths[index] = wIn;
|
|
3392
|
+
});
|
|
3393
|
+
|
|
3394
|
+
// Process header as first row
|
|
3395
|
+
const headerData = headerCells.map(cell => {
|
|
3396
|
+
const style = window.getComputedStyle(cell);
|
|
3397
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
3398
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
3399
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
3400
|
+
|
|
3401
|
+
return {
|
|
3402
|
+
text: cellText,
|
|
3403
|
+
options: {
|
|
3404
|
+
...textStyle,
|
|
3405
|
+
fill: fill || { color: '16A085', transparency: 80 },
|
|
3406
|
+
bold: true,
|
|
3407
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
3408
|
+
border: { type: 'none' }
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3411
|
+
});
|
|
3412
|
+
rows.push(headerData);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
// Find data rows
|
|
3416
|
+
const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
|
|
3417
|
+
dataRows.forEach(row => {
|
|
3418
|
+
const cells = Array.from(row.children);
|
|
3419
|
+
const rowData = cells.map(cell => {
|
|
3420
|
+
const style = window.getComputedStyle(cell);
|
|
3421
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
3422
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
3423
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
3424
|
+
|
|
3425
|
+
// Detect trend colors
|
|
3426
|
+
const className = (cell.className || '').toLowerCase();
|
|
3427
|
+
let textColor = textStyle.color;
|
|
3428
|
+
if (className.includes('up') || className.includes('positive')) {
|
|
3429
|
+
textColor = '16A085';
|
|
3430
|
+
} else if (className.includes('down') || className.includes('negative')) {
|
|
3431
|
+
textColor = 'E74C3C';
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
return {
|
|
3435
|
+
text: cellText,
|
|
3436
|
+
options: {
|
|
3437
|
+
...textStyle,
|
|
3438
|
+
color: textColor,
|
|
3439
|
+
fill: fill,
|
|
3440
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
3441
|
+
border: {
|
|
3442
|
+
type: 'none',
|
|
3443
|
+
bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
};
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
if (rowData.length > 0) {
|
|
3450
|
+
rows.push(rowData);
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
|
|
3454
|
+
// Ensure all rows have same column count
|
|
3455
|
+
const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
|
|
3456
|
+
rows.forEach(row => {
|
|
3457
|
+
while (row.length < maxCols) {
|
|
3458
|
+
row.push({ text: '', options: {} });
|
|
3459
|
+
}
|
|
3460
|
+
});
|
|
3461
|
+
while (colWidths.length < maxCols) {
|
|
3462
|
+
colWidths.push(colWidths[colWidths.length - 1] || 1);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
return { rows, colWidths };
|
|
3466
|
+
}
|
|
3467
|
+
|
|
2734
3468
|
exports.exportToPptx = exportToPptx;
|
|
2735
3469
|
//# sourceMappingURL=dom-to-pptx.cjs.map
|