@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.mjs
CHANGED
|
@@ -309,6 +309,7 @@ function extractTableData(node, scale, options = {}) {
|
|
|
309
309
|
bold: textStyle.bold,
|
|
310
310
|
italic: textStyle.italic,
|
|
311
311
|
underline: textStyle.underline,
|
|
312
|
+
lang: 'zh-CN',
|
|
312
313
|
|
|
313
314
|
fill: fill,
|
|
314
315
|
align: align,
|
|
@@ -383,11 +384,11 @@ function getGradientFallbackColor(bgImage) {
|
|
|
383
384
|
// 3. Find first part that is a color (skip angle/direction)
|
|
384
385
|
for (const part of parts) {
|
|
385
386
|
// Ignore directions (to right) or angles (90deg, 0.5turn)
|
|
386
|
-
if (/^(to\s|[\d
|
|
387
|
+
if (/^(to\s|[\d.]+(deg|rad|turn|grad))/.test(part)) continue;
|
|
387
388
|
|
|
388
389
|
// Extract color: Remove trailing position (e.g. "red 50%" -> "red")
|
|
389
390
|
// Regex matches whitespace + number + unit at end of string
|
|
390
|
-
const colorPart = part.replace(/\s+(-?[\d
|
|
391
|
+
const colorPart = part.replace(/\s+(-?[\d.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
|
|
391
392
|
|
|
392
393
|
// Check if it's not just a number (some gradients might have bare numbers? unlikely in standard syntax)
|
|
393
394
|
if (colorPart) return colorPart;
|
|
@@ -658,6 +659,214 @@ function parseColor(str) {
|
|
|
658
659
|
return { hex, opacity: a };
|
|
659
660
|
}
|
|
660
661
|
|
|
662
|
+
const SUPPORTED_CHART_TYPES = new Set(['area', 'bar', 'line', 'pie', 'doughnut', 'radar', 'scatter']);
|
|
663
|
+
const CHART_TYPE_ALIASES = {
|
|
664
|
+
column: 'bar',
|
|
665
|
+
columnbar: 'bar',
|
|
666
|
+
column3d: 'bar',
|
|
667
|
+
bar3d: 'bar',
|
|
668
|
+
donut: 'doughnut',
|
|
669
|
+
ring: 'doughnut',
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
function normalizeLegendPos(value) {
|
|
673
|
+
if (!value) return null;
|
|
674
|
+
const normalized = value.toString().toLowerCase().replace(/[^a-z]/g, '');
|
|
675
|
+
if (normalized === 'tr' || normalized === 'topright') return 'tr';
|
|
676
|
+
if (normalized === 'top') return 't';
|
|
677
|
+
if (normalized === 'bottom') return 'b';
|
|
678
|
+
if (normalized === 'left') return 'l';
|
|
679
|
+
if (normalized === 'right') return 'r';
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function normalizeChartType(value) {
|
|
684
|
+
if (!value) return null;
|
|
685
|
+
const raw = value.toString().trim().toLowerCase();
|
|
686
|
+
const alias = CHART_TYPE_ALIASES[raw];
|
|
687
|
+
const candidate = alias || raw;
|
|
688
|
+
return SUPPORTED_CHART_TYPES.has(candidate) ? candidate : null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function colorToHex(value) {
|
|
692
|
+
const parsed = parseColor(value);
|
|
693
|
+
return parsed.hex || null;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function resolveChartElement(root, config) {
|
|
697
|
+
if (!config) return null;
|
|
698
|
+
if (config.element instanceof HTMLElement) return config.element;
|
|
699
|
+
if (typeof config.element === 'function') return config.element(root);
|
|
700
|
+
if (typeof config.selector === 'string') {
|
|
701
|
+
return root.querySelector(config.selector);
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function buildSeriesFromSource(source = {}) {
|
|
707
|
+
const fallbackLabels = Array.isArray(source.labels) ? source.labels : [];
|
|
708
|
+
const candidateDatasets = Array.isArray(source.datasets) ? source.datasets : [];
|
|
709
|
+
const series = [];
|
|
710
|
+
|
|
711
|
+
candidateDatasets.forEach((dataset, index) => {
|
|
712
|
+
if (!dataset) return;
|
|
713
|
+
const values = Array.isArray(dataset.values)
|
|
714
|
+
? dataset.values
|
|
715
|
+
: Array.isArray(dataset.data)
|
|
716
|
+
? dataset.data
|
|
717
|
+
: [];
|
|
718
|
+
|
|
719
|
+
if (!values.length) return;
|
|
720
|
+
|
|
721
|
+
const record = {
|
|
722
|
+
name: dataset.name || dataset.label || `Series ${index + 1}`,
|
|
723
|
+
values,
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
if (Array.isArray(dataset.labels) && dataset.labels.length === values.length) {
|
|
727
|
+
record.labels = dataset.labels;
|
|
728
|
+
} else if (fallbackLabels.length === values.length) {
|
|
729
|
+
record.labels = fallbackLabels;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (Array.isArray(dataset.sizes) && dataset.sizes.length === values.length) {
|
|
733
|
+
record.sizes = dataset.sizes;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Chart.js often provides strings/arrays in backgroundColor
|
|
737
|
+
const candidateColor =
|
|
738
|
+
dataset.color || dataset.backgroundColor || dataset.borderColor || dataset.fillColor;
|
|
739
|
+
if (candidateColor) {
|
|
740
|
+
if (Array.isArray(candidateColor)) {
|
|
741
|
+
record.color = candidateColor[0];
|
|
742
|
+
} else {
|
|
743
|
+
record.color = candidateColor;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
series.push(record);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (!series.length) {
|
|
751
|
+
const fallbackValues = Array.isArray(source.values)
|
|
752
|
+
? source.values
|
|
753
|
+
: Array.isArray(source.data)
|
|
754
|
+
? source.data
|
|
755
|
+
: [];
|
|
756
|
+
|
|
757
|
+
if (fallbackValues.length) {
|
|
758
|
+
const fallbackRecord = {
|
|
759
|
+
name: source.name || source.label || 'Series 1',
|
|
760
|
+
values: fallbackValues,
|
|
761
|
+
};
|
|
762
|
+
if (Array.isArray(source.labels) && source.labels.length === fallbackValues.length) {
|
|
763
|
+
fallbackRecord.labels = source.labels;
|
|
764
|
+
} else if (fallbackLabels.length === fallbackValues.length) {
|
|
765
|
+
fallbackRecord.labels = fallbackLabels;
|
|
766
|
+
}
|
|
767
|
+
if (Array.isArray(source.sizes) && source.sizes.length === fallbackValues.length) {
|
|
768
|
+
fallbackRecord.sizes = source.sizes;
|
|
769
|
+
}
|
|
770
|
+
if (source.color) {
|
|
771
|
+
fallbackRecord.color = source.color;
|
|
772
|
+
}
|
|
773
|
+
series.push(fallbackRecord);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return series;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function buildChartOptions(raw, derivedColors) {
|
|
781
|
+
const opts = { ...((raw && raw.chartOptions) || {}) };
|
|
782
|
+
if (raw && raw.showLegend !== undefined) opts.showLegend = raw.showLegend;
|
|
783
|
+
if (raw) {
|
|
784
|
+
const legendTarget = raw.legendPos || raw.legendPosition;
|
|
785
|
+
const normalizedLegend = normalizeLegendPos(legendTarget);
|
|
786
|
+
if (normalizedLegend) opts.legendPos = normalizedLegend;
|
|
787
|
+
}
|
|
788
|
+
if (raw && raw.title) opts.title = raw.title;
|
|
789
|
+
if (raw && raw.altText) opts.altText = raw.altText;
|
|
790
|
+
if (raw && raw.showDataTable !== undefined) opts.showDataTable = raw.showDataTable;
|
|
791
|
+
if (raw && raw.chartColorsOpacity !== undefined) opts.chartColorsOpacity = raw.chartColorsOpacity;
|
|
792
|
+
if (raw && raw.showLabel !== undefined) opts.showLabel = raw.showLabel;
|
|
793
|
+
|
|
794
|
+
const paletteFromConfig =
|
|
795
|
+
raw && Array.isArray(raw.chartColors) ? raw.chartColors.map(colorToHex).filter(Boolean) : [];
|
|
796
|
+
const palette = paletteFromConfig.length ? paletteFromConfig : derivedColors;
|
|
797
|
+
if (palette.length && !opts.chartColors) {
|
|
798
|
+
opts.chartColors = palette;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return opts;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function normalizeChartConfig(raw) {
|
|
805
|
+
if (!raw) return null;
|
|
806
|
+
const chartType = normalizeChartType(raw.chartType || raw.type || raw.chart);
|
|
807
|
+
if (!chartType) return null;
|
|
808
|
+
|
|
809
|
+
let seriesWithColor = [];
|
|
810
|
+
if (Array.isArray(raw.chartData) && raw.chartData.length) {
|
|
811
|
+
seriesWithColor = raw.chartData
|
|
812
|
+
.map((entry, index) => {
|
|
813
|
+
if (!entry || !Array.isArray(entry.values)) return null;
|
|
814
|
+
return {
|
|
815
|
+
...entry,
|
|
816
|
+
name: entry.name || entry.label || `Series ${index + 1}`,
|
|
817
|
+
};
|
|
818
|
+
})
|
|
819
|
+
.filter((entry) => entry && entry.values && entry.values.length);
|
|
820
|
+
} else {
|
|
821
|
+
const source = {
|
|
822
|
+
labels: raw.data?.labels || raw.labels,
|
|
823
|
+
datasets: Array.isArray(raw.data?.datasets)
|
|
824
|
+
? raw.data.datasets
|
|
825
|
+
: Array.isArray(raw.datasets)
|
|
826
|
+
? raw.datasets
|
|
827
|
+
: [],
|
|
828
|
+
values: raw.data?.values || raw.values,
|
|
829
|
+
data: raw.data?.data,
|
|
830
|
+
name: raw.name,
|
|
831
|
+
label: raw.label,
|
|
832
|
+
color: raw.color,
|
|
833
|
+
sizes: raw.data?.sizes || raw.sizes,
|
|
834
|
+
};
|
|
835
|
+
seriesWithColor = buildSeriesFromSource(source);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (!seriesWithColor.length) return null;
|
|
839
|
+
|
|
840
|
+
const derivedColors = seriesWithColor
|
|
841
|
+
.map((dataset) => dataset.color)
|
|
842
|
+
.map(colorToHex)
|
|
843
|
+
.filter(Boolean);
|
|
844
|
+
|
|
845
|
+
const chartOptions = buildChartOptions(raw, derivedColors);
|
|
846
|
+
const chartData = seriesWithColor.map(({ color, ...rest }) => rest);
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
chartType,
|
|
850
|
+
chartData,
|
|
851
|
+
chartOptions,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function buildChartRegistry(root, chartConfigs = []) {
|
|
856
|
+
const registry = new Map();
|
|
857
|
+
if (!root || !Array.isArray(chartConfigs)) return registry;
|
|
858
|
+
|
|
859
|
+
chartConfigs.forEach((config) => {
|
|
860
|
+
const node = resolveChartElement(root, config);
|
|
861
|
+
if (!node) return;
|
|
862
|
+
const normalized = normalizeChartConfig(config);
|
|
863
|
+
if (!normalized || !normalized.chartData || !normalized.chartData.length) return;
|
|
864
|
+
registry.set(node, normalized);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return registry;
|
|
868
|
+
}
|
|
869
|
+
|
|
661
870
|
function getPadding(style, scale) {
|
|
662
871
|
const pxToInch = 1 / 96;
|
|
663
872
|
return [
|
|
@@ -676,6 +885,8 @@ function getSoftEdges(filterStr, scale) {
|
|
|
676
885
|
}
|
|
677
886
|
|
|
678
887
|
const DEFAULT_CJK_FONTS = [
|
|
888
|
+
'Heiti TC',
|
|
889
|
+
'Heiti SC',
|
|
679
890
|
'PingFang SC',
|
|
680
891
|
'Hiragino Sans GB',
|
|
681
892
|
'Microsoft YaHei',
|
|
@@ -727,7 +938,7 @@ function pickFontFace(fontFamily, text, options) {
|
|
|
727
938
|
}
|
|
728
939
|
|
|
729
940
|
const autoMatch = DEFAULT_CJK_FONTS.find((f) => lowered.includes(f.toLowerCase()));
|
|
730
|
-
return autoMatch ||
|
|
941
|
+
return autoMatch || 'Heiti TC';
|
|
731
942
|
}
|
|
732
943
|
|
|
733
944
|
function getTextStyle(style, scale, text = '', options = {}) {
|
|
@@ -758,6 +969,9 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
758
969
|
// And apply the global layout scale.
|
|
759
970
|
lineSpacing = lhPx * 0.75 * scale;
|
|
760
971
|
}
|
|
972
|
+
} else {
|
|
973
|
+
// Default line height when 'normal' - use 1.2 multiplier to match browser default
|
|
974
|
+
lineSpacing = fontSizePx * 1.2 * 0.75 * scale;
|
|
761
975
|
}
|
|
762
976
|
|
|
763
977
|
// --- Spacing (Margins) ---
|
|
@@ -776,10 +990,11 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
776
990
|
return {
|
|
777
991
|
color: colorObj.hex || '000000',
|
|
778
992
|
fontFace: fontFace,
|
|
779
|
-
fontSize: Math.floor(fontSizePx * 0.75 * scale),
|
|
993
|
+
fontSize: Math.max(8, Math.floor(fontSizePx * 0.75 * scale)),
|
|
780
994
|
bold: parseInt(style.fontWeight) >= 600,
|
|
781
995
|
italic: style.fontStyle === 'italic',
|
|
782
996
|
underline: style.textDecoration.includes('underline'),
|
|
997
|
+
lang: 'zh-CN',
|
|
783
998
|
// Only add if we have a valid value
|
|
784
999
|
...(lineSpacing && { lineSpacing }),
|
|
785
1000
|
...(paraSpaceBefore > 0 && { paraSpaceBefore }),
|
|
@@ -792,6 +1007,7 @@ function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
792
1007
|
/**
|
|
793
1008
|
* Determines if a given DOM node is primarily a text container.
|
|
794
1009
|
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
1010
|
+
* Also rejects flex/grid containers with distributed children (space-between/around/evenly).
|
|
795
1011
|
*/
|
|
796
1012
|
function isTextContainer(node) {
|
|
797
1013
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -800,6 +1016,26 @@ function isTextContainer(node) {
|
|
|
800
1016
|
const children = Array.from(node.children);
|
|
801
1017
|
if (children.length === 0) return true;
|
|
802
1018
|
|
|
1019
|
+
// Check if parent is a flex/grid container with special layout
|
|
1020
|
+
// In such cases, children should be treated as separate elements
|
|
1021
|
+
const parentStyle = window.getComputedStyle(node);
|
|
1022
|
+
const display = parentStyle.display;
|
|
1023
|
+
const justifyContent = parentStyle.justifyContent || '';
|
|
1024
|
+
parentStyle.alignItems || '';
|
|
1025
|
+
|
|
1026
|
+
// If parent uses flex/grid with space distribution, don't treat as text container
|
|
1027
|
+
if ((display.includes('flex') || display.includes('grid')) &&
|
|
1028
|
+
(justifyContent.includes('space-between') ||
|
|
1029
|
+
justifyContent.includes('space-around') ||
|
|
1030
|
+
justifyContent.includes('space-evenly'))) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Note: We don't skip text container for align-items: center here.
|
|
1035
|
+
// The valign inheritance is handled in prepareRenderItem with single-child check.
|
|
1036
|
+
// This allows elements like "密钥状态流转" to properly inherit center alignment
|
|
1037
|
+
// while preventing STEP 1/2/3 from being incorrectly centered.
|
|
1038
|
+
|
|
803
1039
|
const isSafeInline = (el) => {
|
|
804
1040
|
// 1. Reject Web Components / Custom Elements
|
|
805
1041
|
if (el.tagName.includes('-')) return false;
|
|
@@ -1358,6 +1594,60 @@ const PptxGenJS = PptxGenJSImport?.default ?? PptxGenJSImport;
|
|
|
1358
1594
|
const PPI = 96;
|
|
1359
1595
|
const PX_TO_INCH = 1 / PPI;
|
|
1360
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* Fix Chinese font attributes (pitchFamily and charset) in PPTX XML.
|
|
1599
|
+
* PptxGenJS sometimes generates incorrect charset="0" for East Asian fonts.
|
|
1600
|
+
* @param {Blob} blob - The PPTX blob
|
|
1601
|
+
* @returns {Promise<Blob>} - Fixed PPTX blob
|
|
1602
|
+
*/
|
|
1603
|
+
async function fixChineseFontAttributes(blob) {
|
|
1604
|
+
const zip = await JSZip.loadAsync(blob);
|
|
1605
|
+
const cjkFonts = ['Heiti TC', 'Heiti SC', 'PingFang SC', 'Microsoft YaHei', 'SimHei', 'SimSun'];
|
|
1606
|
+
|
|
1607
|
+
// Process all slide XML files
|
|
1608
|
+
const slideFiles = Object.keys(zip.files).filter(name =>
|
|
1609
|
+
name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
for (const slidePath of slideFiles) {
|
|
1613
|
+
let xml = await zip.file(slidePath).async('text');
|
|
1614
|
+
let modified = false;
|
|
1615
|
+
|
|
1616
|
+
// Fix <a:ea> and <a:cs> tags for CJK fonts
|
|
1617
|
+
// Replace tags that have charset="0" (incorrect for CJK) with proper values
|
|
1618
|
+
for (const fontName of cjkFonts) {
|
|
1619
|
+
// Fix <a:ea> - East Asian font
|
|
1620
|
+
const eaPattern = new RegExp(
|
|
1621
|
+
`<a:ea typeface="${fontName}"[^>]*>`,
|
|
1622
|
+
'g'
|
|
1623
|
+
);
|
|
1624
|
+
xml = xml.replace(eaPattern, `<a:ea typeface="${fontName}" pitchFamily="34" charset="-122"/>`);
|
|
1625
|
+
|
|
1626
|
+
// Fix <a:cs> - Complex Script font
|
|
1627
|
+
const csPattern = new RegExp(
|
|
1628
|
+
`<a:cs typeface="${fontName}"[^>]*>`,
|
|
1629
|
+
'g'
|
|
1630
|
+
);
|
|
1631
|
+
xml = xml.replace(csPattern, `<a:cs typeface="${fontName}" pitchFamily="34" charset="-120"/>`);
|
|
1632
|
+
|
|
1633
|
+
// Fix <a:latin> - Latin font (if it's a CJK font used as Latin)
|
|
1634
|
+
const latinPattern = new RegExp(
|
|
1635
|
+
`<a:latin typeface="${fontName}"[^>]*>`,
|
|
1636
|
+
'g'
|
|
1637
|
+
);
|
|
1638
|
+
xml = xml.replace(latinPattern, `<a:latin typeface="${fontName}" pitchFamily="34" charset="0"/>`);
|
|
1639
|
+
}
|
|
1640
|
+
modified = true;
|
|
1641
|
+
|
|
1642
|
+
if (modified) {
|
|
1643
|
+
zip.file(slidePath, xml);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Generate new blob
|
|
1648
|
+
return await zip.generateAsync({ type: 'blob' });
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1361
1651
|
/**
|
|
1362
1652
|
* Main export function.
|
|
1363
1653
|
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
@@ -1382,6 +1672,7 @@ async function exportToPptx(target, options = {}) {
|
|
|
1382
1672
|
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
1383
1673
|
const pptx = new PptxConstructor();
|
|
1384
1674
|
pptx.layout = 'LAYOUT_16x9';
|
|
1675
|
+
pptx.language = 'zh-CN';
|
|
1385
1676
|
|
|
1386
1677
|
const elements = Array.isArray(target) ? target : [target];
|
|
1387
1678
|
|
|
@@ -1456,6 +1747,9 @@ async function exportToPptx(target, options = {}) {
|
|
|
1456
1747
|
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
1457
1748
|
}
|
|
1458
1749
|
|
|
1750
|
+
// Fix Chinese font attributes (pitchFamily and charset) in all slide XMLs
|
|
1751
|
+
finalBlob = await fixChineseFontAttributes(finalBlob);
|
|
1752
|
+
|
|
1459
1753
|
// 4. Output Handling
|
|
1460
1754
|
// If skipDownload is NOT true, proceed with browser download
|
|
1461
1755
|
if (!options.skipDownload) {
|
|
@@ -1497,6 +1791,9 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1497
1791
|
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
1498
1792
|
};
|
|
1499
1793
|
|
|
1794
|
+
const chartRegistry = buildChartRegistry(root, globalOptions.chartConfigs);
|
|
1795
|
+
const slideOptions = { ...globalOptions, chartRegistry };
|
|
1796
|
+
|
|
1500
1797
|
const renderQueue = [];
|
|
1501
1798
|
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
1502
1799
|
let domOrderCounter = 0;
|
|
@@ -1532,7 +1829,7 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1532
1829
|
pptx,
|
|
1533
1830
|
currentZ,
|
|
1534
1831
|
nodeStyle,
|
|
1535
|
-
|
|
1832
|
+
slideOptions
|
|
1536
1833
|
);
|
|
1537
1834
|
|
|
1538
1835
|
if (result) {
|
|
@@ -1578,6 +1875,10 @@ async function processSlide(root, slide, pptx, globalOptions = {}) {
|
|
|
1578
1875
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
1579
1876
|
if (item.type === 'image') slide.addImage(item.options);
|
|
1580
1877
|
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
1878
|
+
if (item.type === 'chart') {
|
|
1879
|
+
const chartType = PptxGenJS.ChartType?.[item.chartType] ?? item.chartType;
|
|
1880
|
+
slide.addChart(chartType, item.chartData, item.options);
|
|
1881
|
+
}
|
|
1581
1882
|
if (item.type === 'table') {
|
|
1582
1883
|
slide.addTable(item.tableData.rows, {
|
|
1583
1884
|
x: item.options.x,
|
|
@@ -1778,13 +2079,23 @@ function prepareRenderItem(
|
|
|
1778
2079
|
range.detach();
|
|
1779
2080
|
|
|
1780
2081
|
const style = window.getComputedStyle(parent);
|
|
1781
|
-
|
|
1782
|
-
|
|
2082
|
+
// Use parent element's rect for better width calculation
|
|
2083
|
+
// This is especially important for flex children where text node rect may be too narrow
|
|
2084
|
+
const parentRect = parent.getBoundingClientRect();
|
|
2085
|
+
const useParentRect = parentRect.width > rect.width;
|
|
2086
|
+
let widthPx = useParentRect ? parentRect.width : rect.width;
|
|
2087
|
+
const heightPx = useParentRect ? parentRect.height : rect.height;
|
|
2088
|
+
|
|
2089
|
+
// Add extra width buffer to prevent text wrapping in PPTX due to font rendering differences
|
|
2090
|
+
// Chinese characters and certain fonts need more space in PPTX than in browser
|
|
2091
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
1783
2092
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
1784
2093
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
1785
2094
|
|
|
1786
|
-
|
|
1787
|
-
const
|
|
2095
|
+
// Use parent rect's position if we're using parent dimensions
|
|
2096
|
+
const sourceRect = useParentRect ? parentRect : rect;
|
|
2097
|
+
const x = config.offX + (sourceRect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
2098
|
+
const y = config.offY + (sourceRect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
1788
2099
|
|
|
1789
2100
|
return {
|
|
1790
2101
|
items: [
|
|
@@ -1823,7 +2134,7 @@ function prepareRenderItem(
|
|
|
1823
2134
|
const elementOpacity = parseFloat(style.opacity);
|
|
1824
2135
|
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
1825
2136
|
|
|
1826
|
-
|
|
2137
|
+
let widthPx = node.offsetWidth || rect.width;
|
|
1827
2138
|
const heightPx = node.offsetHeight || rect.height;
|
|
1828
2139
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
1829
2140
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
@@ -1837,8 +2148,41 @@ function prepareRenderItem(
|
|
|
1837
2148
|
|
|
1838
2149
|
const items = [];
|
|
1839
2150
|
|
|
1840
|
-
|
|
1841
|
-
|
|
2151
|
+
const chartConfig = globalOptions.chartRegistry?.get(node);
|
|
2152
|
+
if (chartConfig) {
|
|
2153
|
+
const chartOptions = {
|
|
2154
|
+
...chartConfig.chartOptions,
|
|
2155
|
+
x,
|
|
2156
|
+
y,
|
|
2157
|
+
w: unrotatedW,
|
|
2158
|
+
h: unrotatedH,
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
return {
|
|
2162
|
+
items: [
|
|
2163
|
+
{
|
|
2164
|
+
type: 'chart',
|
|
2165
|
+
zIndex: effectiveZIndex,
|
|
2166
|
+
domOrder,
|
|
2167
|
+
chartType: chartConfig.chartType,
|
|
2168
|
+
chartData: chartConfig.chartData,
|
|
2169
|
+
options: chartOptions,
|
|
2170
|
+
},
|
|
2171
|
+
],
|
|
2172
|
+
stopRecursion: true,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// --- Handle both native TABLE and div-based table structures ---
|
|
2177
|
+
const isTableLike = node.tagName === 'TABLE' || detectTableLikeStructure(node);
|
|
2178
|
+
if (isTableLike) {
|
|
2179
|
+
let tableData;
|
|
2180
|
+
if (node.tagName === 'TABLE') {
|
|
2181
|
+
tableData = extractTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
2182
|
+
} else {
|
|
2183
|
+
// Extract data from div-based table structure
|
|
2184
|
+
tableData = extractDivTableData(node, config.scale, { ...globalOptions, root: config.root });
|
|
2185
|
+
}
|
|
1842
2186
|
const cellBgItems = [];
|
|
1843
2187
|
const renderCellBg = globalOptions.tableConfig?.renderCellBackgrounds !== false;
|
|
1844
2188
|
|
|
@@ -2182,6 +2526,35 @@ function prepareRenderItem(
|
|
|
2182
2526
|
return { items: [item], job, stopRecursion: true };
|
|
2183
2527
|
}
|
|
2184
2528
|
|
|
2529
|
+
// --- Handle vertical stat cards (like .mini-stat with number + label) ---
|
|
2530
|
+
if (isVerticalStatCard(node)) {
|
|
2531
|
+
// Capture the entire stat card as an image to preserve vertical layout
|
|
2532
|
+
const item = {
|
|
2533
|
+
type: 'image',
|
|
2534
|
+
zIndex,
|
|
2535
|
+
domOrder,
|
|
2536
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
2537
|
+
};
|
|
2538
|
+
const job = async () => {
|
|
2539
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
2540
|
+
if (pngData) item.options.data = pngData;
|
|
2541
|
+
else item.skip = true;
|
|
2542
|
+
};
|
|
2543
|
+
return { items: [item], job, stopRecursion: true };
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// --- Handle absolutely positioned children that overflow parent ---
|
|
2547
|
+
// Check if this element has absolutely positioned children that extend outside
|
|
2548
|
+
const overflowingChildren = detectOverflowingChildren(node);
|
|
2549
|
+
if (overflowingChildren.length > 0) {
|
|
2550
|
+
// Process this node normally, but also capture overflowing children separately
|
|
2551
|
+
const baseResult = processOverflowingContent(
|
|
2552
|
+
node, overflowingChildren, config, domOrder, pptx, zIndex);
|
|
2553
|
+
if (baseResult) {
|
|
2554
|
+
return baseResult;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2185
2558
|
// Radii logic
|
|
2186
2559
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
2187
2560
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
@@ -2284,6 +2657,27 @@ function prepareRenderItem(
|
|
|
2284
2657
|
let textPayload = null;
|
|
2285
2658
|
const isText = isTextContainer(node);
|
|
2286
2659
|
|
|
2660
|
+
// Add extra width buffer for inline text elements to prevent wrapping in PPTX
|
|
2661
|
+
// This is especially needed for Chinese text and certain fonts
|
|
2662
|
+
// Only apply to elements that are direct children of flex containers with space distribution
|
|
2663
|
+
// and don't have a parent with visible background (to avoid overflowing card boundaries)
|
|
2664
|
+
if (isText && !bgColorObj.hex && !hasBorder && !hasShadow) {
|
|
2665
|
+
const parentEl = node.parentElement;
|
|
2666
|
+
if (parentEl) {
|
|
2667
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
2668
|
+
const parentDisplay = parentStyle.display || '';
|
|
2669
|
+
const parentJustify = parentStyle.justifyContent || '';
|
|
2670
|
+
const isFlexWithSpaceDist = (parentDisplay.includes('flex') || parentDisplay.includes('grid')) &&
|
|
2671
|
+
(parentJustify.includes('space-between') || parentJustify.includes('space-around') || parentJustify.includes('space-evenly'));
|
|
2672
|
+
|
|
2673
|
+
if (isFlexWithSpaceDist) {
|
|
2674
|
+
widthPx = widthPx * 1.3; // Add 30% extra width
|
|
2675
|
+
// Recalculate w with the new width
|
|
2676
|
+
w = widthPx * PX_TO_INCH * config.scale;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2287
2681
|
if (isText) {
|
|
2288
2682
|
const textParts = [];
|
|
2289
2683
|
let trimNextLeading = false;
|
|
@@ -2340,8 +2734,71 @@ function prepareRenderItem(
|
|
|
2340
2734
|
let align = style.textAlign || 'left';
|
|
2341
2735
|
if (align === 'start') align = 'left';
|
|
2342
2736
|
if (align === 'end') align = 'right';
|
|
2737
|
+
|
|
2738
|
+
// Fix: If this element is a flex/grid child and has no explicit text-align,
|
|
2739
|
+
// force left alignment to match HTML default behavior
|
|
2740
|
+
if (node.parentElement && (!style.textAlign || style.textAlign === 'start')) {
|
|
2741
|
+
const parentStyle = window.getComputedStyle(node.parentElement);
|
|
2742
|
+
if (parentStyle.display.includes('flex') || parentStyle.display.includes('grid')) {
|
|
2743
|
+
align = 'left';
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Detect badge/pill buttons (high border-radius + short text) and auto-center them
|
|
2748
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
2749
|
+
const height = parseFloat(style.height) || node.offsetHeight;
|
|
2750
|
+
const textContent = node.textContent.trim();
|
|
2751
|
+
const className = (node.className || '').toLowerCase();
|
|
2752
|
+
|
|
2753
|
+
// Real badges/pills typically have visible styling (background, border, or large border-radius)
|
|
2754
|
+
const hasVisibleBackground = bgColorObj.hex && bgColorObj.opacity > 0.1;
|
|
2755
|
+
const hasVisibleBorder = borderWidth > 0;
|
|
2756
|
+
const hasLargeBorderRadius = borderRadius >= height / 2;
|
|
2757
|
+
const hasBadgeClass = className.includes('badge') || className.includes('pill');
|
|
2758
|
+
|
|
2759
|
+
// Only consider it a badge if it has visual styling AND short text
|
|
2760
|
+
const isLikelyBadge =
|
|
2761
|
+
(hasLargeBorderRadius || hasBadgeClass) &&
|
|
2762
|
+
textContent.length <= 10 &&
|
|
2763
|
+
(hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius);
|
|
2764
|
+
|
|
2765
|
+
if (isLikelyBadge) {
|
|
2766
|
+
align = 'center';
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2343
2769
|
let valign = 'top';
|
|
2344
|
-
|
|
2770
|
+
|
|
2771
|
+
// For flex items, valign should be determined by PARENT's align-items, not self
|
|
2772
|
+
// Self's align-items controls how children are aligned, not self's position in parent
|
|
2773
|
+
const parentEl = node.parentElement;
|
|
2774
|
+
if (parentEl) {
|
|
2775
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
2776
|
+
if (parentStyle.display.includes('flex')) {
|
|
2777
|
+
// Parent's align-items controls this element's cross-axis alignment
|
|
2778
|
+
if (parentStyle.alignItems === 'center') {
|
|
2779
|
+
valign = 'middle';
|
|
2780
|
+
} else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
|
|
2781
|
+
valign = 'bottom';
|
|
2782
|
+
}
|
|
2783
|
+
// Default (stretch, flex-start) keeps valign = 'top'
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// If element is not a flex item (no flex parent), then its own align-items
|
|
2788
|
+
// might indicate self-centering intent for single-element containers
|
|
2789
|
+
if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
|
|
2790
|
+
valign = 'middle';
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// Auto-center vertically for likely badges
|
|
2794
|
+
if (isLikelyBadge && valign !== 'middle') {
|
|
2795
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
2796
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
2797
|
+
// If padding is relatively even, assume vertical centering is intended
|
|
2798
|
+
if (Math.abs(paddingTop - paddingBottom) <= 4) {
|
|
2799
|
+
valign = 'middle';
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2345
2802
|
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
2346
2803
|
|
|
2347
2804
|
const pt = parseFloat(style.paddingTop) || 0;
|
|
@@ -2701,5 +3158,282 @@ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder)
|
|
|
2701
3158
|
return items;
|
|
2702
3159
|
}
|
|
2703
3160
|
|
|
3161
|
+
/**
|
|
3162
|
+
* Detect absolutely positioned children that overflow their parent
|
|
3163
|
+
* This handles cases like chart value labels positioned above bars
|
|
3164
|
+
*/
|
|
3165
|
+
function detectOverflowingChildren(node) {
|
|
3166
|
+
const overflowing = [];
|
|
3167
|
+
const parentRect = node.getBoundingClientRect();
|
|
3168
|
+
|
|
3169
|
+
// Only check for elements with specific class patterns that suggest chart values
|
|
3170
|
+
const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
|
|
3171
|
+
|
|
3172
|
+
children.forEach(child => {
|
|
3173
|
+
const childStyle = window.getComputedStyle(child);
|
|
3174
|
+
const childRect = child.getBoundingClientRect();
|
|
3175
|
+
|
|
3176
|
+
// Check if absolutely positioned and outside parent bounds
|
|
3177
|
+
if (childStyle.position === 'absolute') {
|
|
3178
|
+
const isOutside =
|
|
3179
|
+
childRect.bottom < parentRect.top ||
|
|
3180
|
+
childRect.top > parentRect.bottom ||
|
|
3181
|
+
childRect.right < parentRect.left ||
|
|
3182
|
+
childRect.left > parentRect.right;
|
|
3183
|
+
|
|
3184
|
+
if (isOutside || childRect.top < parentRect.top) {
|
|
3185
|
+
overflowing.push(child);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
});
|
|
3189
|
+
|
|
3190
|
+
return overflowing;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
/**
|
|
3194
|
+
* Process content with overflowing children
|
|
3195
|
+
* Captures the entire visual area including overflowing elements
|
|
3196
|
+
*/
|
|
3197
|
+
function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
|
|
3198
|
+
// Calculate bounding box that includes all overflowing children
|
|
3199
|
+
const rect = node.getBoundingClientRect();
|
|
3200
|
+
let minX = rect.left;
|
|
3201
|
+
let minY = rect.top;
|
|
3202
|
+
let maxX = rect.right;
|
|
3203
|
+
let maxY = rect.bottom;
|
|
3204
|
+
|
|
3205
|
+
overflowingChildren.forEach(child => {
|
|
3206
|
+
const childRect = child.getBoundingClientRect();
|
|
3207
|
+
minX = Math.min(minX, childRect.left);
|
|
3208
|
+
minY = Math.min(minY, childRect.top);
|
|
3209
|
+
maxX = Math.max(maxX, childRect.right);
|
|
3210
|
+
maxY = Math.max(maxY, childRect.bottom);
|
|
3211
|
+
});
|
|
3212
|
+
|
|
3213
|
+
const totalWidth = maxX - minX;
|
|
3214
|
+
const totalHeight = maxY - minY;
|
|
3215
|
+
|
|
3216
|
+
// Use html2canvas to capture the entire area
|
|
3217
|
+
const item = {
|
|
3218
|
+
type: 'image',
|
|
3219
|
+
zIndex,
|
|
3220
|
+
domOrder,
|
|
3221
|
+
options: {
|
|
3222
|
+
x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
|
|
3223
|
+
y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
|
|
3224
|
+
w: totalWidth * PX_TO_INCH * config.scale,
|
|
3225
|
+
h: totalHeight * PX_TO_INCH * config.scale,
|
|
3226
|
+
data: null
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
const job = async () => {
|
|
3231
|
+
try {
|
|
3232
|
+
// Temporarily adjust node position for capture
|
|
3233
|
+
const originalPosition = node.style.position;
|
|
3234
|
+
const originalTransform = node.style.transform;
|
|
3235
|
+
|
|
3236
|
+
node.style.position = 'relative';
|
|
3237
|
+
node.style.transform = 'none';
|
|
3238
|
+
|
|
3239
|
+
const canvas = await html2canvas(node, {
|
|
3240
|
+
backgroundColor: null,
|
|
3241
|
+
logging: false,
|
|
3242
|
+
scale: 2,
|
|
3243
|
+
useCORS: true,
|
|
3244
|
+
x: minX - rect.left,
|
|
3245
|
+
y: minY - rect.top,
|
|
3246
|
+
width: totalWidth,
|
|
3247
|
+
height: totalHeight
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
// Restore original styles
|
|
3251
|
+
node.style.position = originalPosition;
|
|
3252
|
+
node.style.transform = originalTransform;
|
|
3253
|
+
|
|
3254
|
+
item.options.data = canvas.toDataURL('image/png');
|
|
3255
|
+
} catch (e) {
|
|
3256
|
+
console.warn('Failed to capture overflowing content:', e);
|
|
3257
|
+
item.skip = true;
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
return { items: [item], job, stopRecursion: true };
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
/**
|
|
3265
|
+
* Detect vertical stat cards (like .mini-stat with number above label)
|
|
3266
|
+
* These have block-level children that should stack vertically
|
|
3267
|
+
*/
|
|
3268
|
+
function isVerticalStatCard(node) {
|
|
3269
|
+
const className = (node.className || '').toLowerCase();
|
|
3270
|
+
|
|
3271
|
+
// Check for stat-like class names
|
|
3272
|
+
const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
|
|
3273
|
+
const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
|
|
3274
|
+
|
|
3275
|
+
if (!hasStatClass) return false;
|
|
3276
|
+
|
|
3277
|
+
const children = Array.from(node.children);
|
|
3278
|
+
if (children.length !== 2) return false;
|
|
3279
|
+
|
|
3280
|
+
// Check if children are likely number + label pair
|
|
3281
|
+
const child1 = children[0];
|
|
3282
|
+
const child2 = children[1];
|
|
3283
|
+
|
|
3284
|
+
const style1 = window.getComputedStyle(child1);
|
|
3285
|
+
const style2 = window.getComputedStyle(child2);
|
|
3286
|
+
|
|
3287
|
+
// Both should be block elements (or have block-like display)
|
|
3288
|
+
const isBlock1 = style1.display === 'block' || style1.display === 'flex';
|
|
3289
|
+
const isBlock2 = style2.display === 'block' || style2.display === 'flex';
|
|
3290
|
+
|
|
3291
|
+
if (!isBlock1 || !isBlock2) return false;
|
|
3292
|
+
|
|
3293
|
+
// First child should have larger font (the number)
|
|
3294
|
+
const fontSize1 = parseFloat(style1.fontSize) || 0;
|
|
3295
|
+
const fontSize2 = parseFloat(style2.fontSize) || 0;
|
|
3296
|
+
|
|
3297
|
+
// Number should be larger than label, or at least bold
|
|
3298
|
+
const isBold1 = parseInt(style1.fontWeight) >= 600;
|
|
3299
|
+
|
|
3300
|
+
return fontSize1 >= fontSize2 || isBold1;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
/**
|
|
3304
|
+
* Detect if a div structure looks like a table
|
|
3305
|
+
* Checks for table-like class names and structure patterns
|
|
3306
|
+
*/
|
|
3307
|
+
function detectTableLikeStructure(node) {
|
|
3308
|
+
const className = (node.className || '').toLowerCase();
|
|
3309
|
+
|
|
3310
|
+
// Check for table-related class names
|
|
3311
|
+
const tableIndicators = ['table', 'data-table', 'grid', 'list'];
|
|
3312
|
+
const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
|
|
3313
|
+
|
|
3314
|
+
if (!hasTableClass) return false;
|
|
3315
|
+
|
|
3316
|
+
// Check structure: should have header row and data rows
|
|
3317
|
+
const children = Array.from(node.children);
|
|
3318
|
+
|
|
3319
|
+
// Look for header-like element
|
|
3320
|
+
const hasHeader = children.some(child =>
|
|
3321
|
+
(child.className || '').toLowerCase().includes('header') ||
|
|
3322
|
+
child.tagName === 'THEAD'
|
|
3323
|
+
);
|
|
3324
|
+
|
|
3325
|
+
// Look for row-like elements
|
|
3326
|
+
const hasRows = children.some(child =>
|
|
3327
|
+
(child.className || '').toLowerCase().includes('row') ||
|
|
3328
|
+
child.tagName === 'TR' ||
|
|
3329
|
+
child.tagName === 'TBODY'
|
|
3330
|
+
);
|
|
3331
|
+
|
|
3332
|
+
// Check for cell-like structure in children
|
|
3333
|
+
const hasCellStructure = children.some(child => {
|
|
3334
|
+
const childChildren = Array.from(child.children);
|
|
3335
|
+
return childChildren.some(grandchild =>
|
|
3336
|
+
(grandchild.className || '').toLowerCase().includes('cell') ||
|
|
3337
|
+
grandchild.className.toLowerCase().includes('col') ||
|
|
3338
|
+
grandchild.className.toLowerCase().includes('td')
|
|
3339
|
+
);
|
|
3340
|
+
});
|
|
3341
|
+
|
|
3342
|
+
return (hasHeader || hasRows) && hasCellStructure;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
/**
|
|
3346
|
+
* Extract table data from div-based table structure
|
|
3347
|
+
* Similar to extractTableData but for div grids
|
|
3348
|
+
*/
|
|
3349
|
+
function extractDivTableData(node, scale, options = {}) {
|
|
3350
|
+
const rows = [];
|
|
3351
|
+
const colWidths = [];
|
|
3352
|
+
const root = options.root || null;
|
|
3353
|
+
|
|
3354
|
+
// Find header row
|
|
3355
|
+
const headerRow = node.querySelector('.table-header, [class*="header"]');
|
|
3356
|
+
if (headerRow) {
|
|
3357
|
+
const headerCells = Array.from(headerRow.children);
|
|
3358
|
+
headerCells.forEach((cell, index) => {
|
|
3359
|
+
const rect = cell.getBoundingClientRect();
|
|
3360
|
+
const wIn = rect.width * (1 / 96) * scale;
|
|
3361
|
+
colWidths[index] = wIn;
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
// Process header as first row
|
|
3365
|
+
const headerData = headerCells.map(cell => {
|
|
3366
|
+
const style = window.getComputedStyle(cell);
|
|
3367
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
3368
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
3369
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
3370
|
+
|
|
3371
|
+
return {
|
|
3372
|
+
text: cellText,
|
|
3373
|
+
options: {
|
|
3374
|
+
...textStyle,
|
|
3375
|
+
fill: fill || { color: '16A085', transparency: 80 },
|
|
3376
|
+
bold: true,
|
|
3377
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
3378
|
+
border: { type: 'none' }
|
|
3379
|
+
}
|
|
3380
|
+
};
|
|
3381
|
+
});
|
|
3382
|
+
rows.push(headerData);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// Find data rows
|
|
3386
|
+
const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
|
|
3387
|
+
dataRows.forEach(row => {
|
|
3388
|
+
const cells = Array.from(row.children);
|
|
3389
|
+
const rowData = cells.map(cell => {
|
|
3390
|
+
const style = window.getComputedStyle(cell);
|
|
3391
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
3392
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
3393
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
3394
|
+
|
|
3395
|
+
// Detect trend colors
|
|
3396
|
+
const className = (cell.className || '').toLowerCase();
|
|
3397
|
+
let textColor = textStyle.color;
|
|
3398
|
+
if (className.includes('up') || className.includes('positive')) {
|
|
3399
|
+
textColor = '16A085';
|
|
3400
|
+
} else if (className.includes('down') || className.includes('negative')) {
|
|
3401
|
+
textColor = 'E74C3C';
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
return {
|
|
3405
|
+
text: cellText,
|
|
3406
|
+
options: {
|
|
3407
|
+
...textStyle,
|
|
3408
|
+
color: textColor,
|
|
3409
|
+
fill: fill,
|
|
3410
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
3411
|
+
border: {
|
|
3412
|
+
type: 'none',
|
|
3413
|
+
bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
};
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
if (rowData.length > 0) {
|
|
3420
|
+
rows.push(rowData);
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
// Ensure all rows have same column count
|
|
3425
|
+
const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
|
|
3426
|
+
rows.forEach(row => {
|
|
3427
|
+
while (row.length < maxCols) {
|
|
3428
|
+
row.push({ text: '', options: {} });
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
while (colWidths.length < maxCols) {
|
|
3432
|
+
colWidths.push(colWidths[colWidths.length - 1] || 1);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
return { rows, colWidths };
|
|
3436
|
+
}
|
|
3437
|
+
|
|
2704
3438
|
export { exportToPptx };
|
|
2705
3439
|
//# sourceMappingURL=dom-to-pptx.mjs.map
|