@microlee666/dom-to-pptx 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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\.]+(deg|rad|turn|grad))/.test(part)) continue;
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\.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
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 || primary;
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
- globalOptions
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
- const widthPx = rect.width;
1812
- const heightPx = rect.height;
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
- const x = config.offX + (rect.left - config.rootX) * PX_TO_INCH * config.scale;
1817
- const y = config.offY + (rect.top - config.rootY) * PX_TO_INCH * config.scale;
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
- const widthPx = node.offsetWidth || rect.width;
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
- if (node.tagName === 'TABLE') {
1871
- const tableData = extractTableData(node, config.scale, { ...globalOptions, root: config.root });
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,91 @@ 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) || hasGradient;
2785
+ const hasVisibleBorder = borderWidth > 0;
2786
+ const hasLargeBorderRadius = borderRadius >= height / 2;
2787
+ const hasBadgeClass = className.includes('badge') || className.includes('pill') || className.includes('tag');
2788
+
2789
+ // Check if it's a small tag/label with rounded corners and short text
2790
+ const paddingLeft = parseFloat(style.paddingLeft) || 0;
2791
+ const paddingRight = parseFloat(style.paddingRight) || 0;
2792
+ const paddingTop = parseFloat(style.paddingTop) || 0;
2793
+ const paddingBottom = parseFloat(style.paddingBottom) || 0;
2794
+ const hasSmallPadding = Math.max(paddingLeft, paddingRight, paddingTop, paddingBottom) <= 12;
2795
+ const hasEvenPadding = Math.abs(paddingTop - paddingBottom) <= 4 && Math.abs(paddingLeft - paddingRight) <= 8;
2796
+ const hasRoundedCorners = borderRadius >= 3;
2797
+ const isInlineBlock = style.display === 'inline-block' || style.display === 'inline-flex';
2798
+
2799
+ // Small tag detection: inline-block with background, rounded corners, small even padding, and short text
2800
+ const isSmallTag =
2801
+ isInlineBlock &&
2802
+ hasVisibleBackground &&
2803
+ hasRoundedCorners &&
2804
+ hasSmallPadding &&
2805
+ hasEvenPadding &&
2806
+ textContent.length <= 10;
2807
+
2808
+ // Only consider it a badge if it has visual styling AND short text
2809
+ const isLikelyBadge =
2810
+ ((hasLargeBorderRadius || hasBadgeClass) &&
2811
+ textContent.length <= 10 &&
2812
+ (hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius)) ||
2813
+ isSmallTag;
2814
+
2815
+ if (isLikelyBadge) {
2816
+ align = 'center';
2817
+ }
2818
+
2373
2819
  let valign = 'top';
2374
- if (style.alignItems === 'center') valign = 'middle';
2820
+
2821
+ // For flex items, valign should be determined by PARENT's align-items, not self
2822
+ // Self's align-items controls how children are aligned, not self's position in parent
2823
+ const parentEl = node.parentElement;
2824
+ if (parentEl) {
2825
+ const parentStyle = window.getComputedStyle(parentEl);
2826
+ if (parentStyle.display.includes('flex')) {
2827
+ // Parent's align-items controls this element's cross-axis alignment
2828
+ if (parentStyle.alignItems === 'center') {
2829
+ valign = 'middle';
2830
+ } else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
2831
+ valign = 'bottom';
2832
+ }
2833
+ // Default (stretch, flex-start) keeps valign = 'top'
2834
+ }
2835
+ }
2836
+
2837
+ // If element is not a flex item (no flex parent), then its own align-items
2838
+ // might indicate self-centering intent for single-element containers
2839
+ if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
2840
+ valign = 'middle';
2841
+ }
2842
+
2843
+ // Auto-center vertically for likely badges
2844
+ if (isLikelyBadge && valign !== 'middle') {
2845
+ const paddingTop = parseFloat(style.paddingTop) || 0;
2846
+ const paddingBottom = parseFloat(style.paddingBottom) || 0;
2847
+ // If padding is relatively even, assume vertical centering is intended
2848
+ if (Math.abs(paddingTop - paddingBottom) <= 4) {
2849
+ valign = 'middle';
2850
+ }
2851
+ }
2375
2852
  if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
2376
2853
 
2377
2854
  const pt = parseFloat(style.paddingTop) || 0;
@@ -2731,5 +3208,282 @@ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder)
2731
3208
  return items;
2732
3209
  }
2733
3210
 
3211
+ /**
3212
+ * Detect absolutely positioned children that overflow their parent
3213
+ * This handles cases like chart value labels positioned above bars
3214
+ */
3215
+ function detectOverflowingChildren(node) {
3216
+ const overflowing = [];
3217
+ const parentRect = node.getBoundingClientRect();
3218
+
3219
+ // Only check for elements with specific class patterns that suggest chart values
3220
+ const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
3221
+
3222
+ children.forEach(child => {
3223
+ const childStyle = window.getComputedStyle(child);
3224
+ const childRect = child.getBoundingClientRect();
3225
+
3226
+ // Check if absolutely positioned and outside parent bounds
3227
+ if (childStyle.position === 'absolute') {
3228
+ const isOutside =
3229
+ childRect.bottom < parentRect.top ||
3230
+ childRect.top > parentRect.bottom ||
3231
+ childRect.right < parentRect.left ||
3232
+ childRect.left > parentRect.right;
3233
+
3234
+ if (isOutside || childRect.top < parentRect.top) {
3235
+ overflowing.push(child);
3236
+ }
3237
+ }
3238
+ });
3239
+
3240
+ return overflowing;
3241
+ }
3242
+
3243
+ /**
3244
+ * Process content with overflowing children
3245
+ * Captures the entire visual area including overflowing elements
3246
+ */
3247
+ function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
3248
+ // Calculate bounding box that includes all overflowing children
3249
+ const rect = node.getBoundingClientRect();
3250
+ let minX = rect.left;
3251
+ let minY = rect.top;
3252
+ let maxX = rect.right;
3253
+ let maxY = rect.bottom;
3254
+
3255
+ overflowingChildren.forEach(child => {
3256
+ const childRect = child.getBoundingClientRect();
3257
+ minX = Math.min(minX, childRect.left);
3258
+ minY = Math.min(minY, childRect.top);
3259
+ maxX = Math.max(maxX, childRect.right);
3260
+ maxY = Math.max(maxY, childRect.bottom);
3261
+ });
3262
+
3263
+ const totalWidth = maxX - minX;
3264
+ const totalHeight = maxY - minY;
3265
+
3266
+ // Use html2canvas to capture the entire area
3267
+ const item = {
3268
+ type: 'image',
3269
+ zIndex,
3270
+ domOrder,
3271
+ options: {
3272
+ x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
3273
+ y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
3274
+ w: totalWidth * PX_TO_INCH * config.scale,
3275
+ h: totalHeight * PX_TO_INCH * config.scale,
3276
+ data: null
3277
+ }
3278
+ };
3279
+
3280
+ const job = async () => {
3281
+ try {
3282
+ // Temporarily adjust node position for capture
3283
+ const originalPosition = node.style.position;
3284
+ const originalTransform = node.style.transform;
3285
+
3286
+ node.style.position = 'relative';
3287
+ node.style.transform = 'none';
3288
+
3289
+ const canvas = await html2canvas__default["default"](node, {
3290
+ backgroundColor: null,
3291
+ logging: false,
3292
+ scale: 2,
3293
+ useCORS: true,
3294
+ x: minX - rect.left,
3295
+ y: minY - rect.top,
3296
+ width: totalWidth,
3297
+ height: totalHeight
3298
+ });
3299
+
3300
+ // Restore original styles
3301
+ node.style.position = originalPosition;
3302
+ node.style.transform = originalTransform;
3303
+
3304
+ item.options.data = canvas.toDataURL('image/png');
3305
+ } catch (e) {
3306
+ console.warn('Failed to capture overflowing content:', e);
3307
+ item.skip = true;
3308
+ }
3309
+ };
3310
+
3311
+ return { items: [item], job, stopRecursion: true };
3312
+ }
3313
+
3314
+ /**
3315
+ * Detect vertical stat cards (like .mini-stat with number above label)
3316
+ * These have block-level children that should stack vertically
3317
+ */
3318
+ function isVerticalStatCard(node) {
3319
+ const className = (node.className || '').toLowerCase();
3320
+
3321
+ // Check for stat-like class names
3322
+ const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
3323
+ const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
3324
+
3325
+ if (!hasStatClass) return false;
3326
+
3327
+ const children = Array.from(node.children);
3328
+ if (children.length !== 2) return false;
3329
+
3330
+ // Check if children are likely number + label pair
3331
+ const child1 = children[0];
3332
+ const child2 = children[1];
3333
+
3334
+ const style1 = window.getComputedStyle(child1);
3335
+ const style2 = window.getComputedStyle(child2);
3336
+
3337
+ // Both should be block elements (or have block-like display)
3338
+ const isBlock1 = style1.display === 'block' || style1.display === 'flex';
3339
+ const isBlock2 = style2.display === 'block' || style2.display === 'flex';
3340
+
3341
+ if (!isBlock1 || !isBlock2) return false;
3342
+
3343
+ // First child should have larger font (the number)
3344
+ const fontSize1 = parseFloat(style1.fontSize) || 0;
3345
+ const fontSize2 = parseFloat(style2.fontSize) || 0;
3346
+
3347
+ // Number should be larger than label, or at least bold
3348
+ const isBold1 = parseInt(style1.fontWeight) >= 600;
3349
+
3350
+ return fontSize1 >= fontSize2 || isBold1;
3351
+ }
3352
+
3353
+ /**
3354
+ * Detect if a div structure looks like a table
3355
+ * Checks for table-like class names and structure patterns
3356
+ */
3357
+ function detectTableLikeStructure(node) {
3358
+ const className = (node.className || '').toLowerCase();
3359
+
3360
+ // Check for table-related class names
3361
+ const tableIndicators = ['table', 'data-table', 'grid', 'list'];
3362
+ const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
3363
+
3364
+ if (!hasTableClass) return false;
3365
+
3366
+ // Check structure: should have header row and data rows
3367
+ const children = Array.from(node.children);
3368
+
3369
+ // Look for header-like element
3370
+ const hasHeader = children.some(child =>
3371
+ (child.className || '').toLowerCase().includes('header') ||
3372
+ child.tagName === 'THEAD'
3373
+ );
3374
+
3375
+ // Look for row-like elements
3376
+ const hasRows = children.some(child =>
3377
+ (child.className || '').toLowerCase().includes('row') ||
3378
+ child.tagName === 'TR' ||
3379
+ child.tagName === 'TBODY'
3380
+ );
3381
+
3382
+ // Check for cell-like structure in children
3383
+ const hasCellStructure = children.some(child => {
3384
+ const childChildren = Array.from(child.children);
3385
+ return childChildren.some(grandchild =>
3386
+ (grandchild.className || '').toLowerCase().includes('cell') ||
3387
+ grandchild.className.toLowerCase().includes('col') ||
3388
+ grandchild.className.toLowerCase().includes('td')
3389
+ );
3390
+ });
3391
+
3392
+ return (hasHeader || hasRows) && hasCellStructure;
3393
+ }
3394
+
3395
+ /**
3396
+ * Extract table data from div-based table structure
3397
+ * Similar to extractTableData but for div grids
3398
+ */
3399
+ function extractDivTableData(node, scale, options = {}) {
3400
+ const rows = [];
3401
+ const colWidths = [];
3402
+ const root = options.root || null;
3403
+
3404
+ // Find header row
3405
+ const headerRow = node.querySelector('.table-header, [class*="header"]');
3406
+ if (headerRow) {
3407
+ const headerCells = Array.from(headerRow.children);
3408
+ headerCells.forEach((cell, index) => {
3409
+ const rect = cell.getBoundingClientRect();
3410
+ const wIn = rect.width * (1 / 96) * scale;
3411
+ colWidths[index] = wIn;
3412
+ });
3413
+
3414
+ // Process header as first row
3415
+ const headerData = headerCells.map(cell => {
3416
+ const style = window.getComputedStyle(cell);
3417
+ const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
3418
+ const textStyle = getTextStyle(style, scale, cellText, options);
3419
+ const fill = computeTableCellFill(style, cell, root, options);
3420
+
3421
+ return {
3422
+ text: cellText,
3423
+ options: {
3424
+ ...textStyle,
3425
+ fill: fill || { color: '16A085', transparency: 80 },
3426
+ bold: true,
3427
+ align: style.textAlign === 'center' ? 'center' : 'left',
3428
+ border: { type: 'none' }
3429
+ }
3430
+ };
3431
+ });
3432
+ rows.push(headerData);
3433
+ }
3434
+
3435
+ // Find data rows
3436
+ const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
3437
+ dataRows.forEach(row => {
3438
+ const cells = Array.from(row.children);
3439
+ const rowData = cells.map(cell => {
3440
+ const style = window.getComputedStyle(cell);
3441
+ const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
3442
+ const textStyle = getTextStyle(style, scale, cellText, options);
3443
+ const fill = computeTableCellFill(style, cell, root, options);
3444
+
3445
+ // Detect trend colors
3446
+ const className = (cell.className || '').toLowerCase();
3447
+ let textColor = textStyle.color;
3448
+ if (className.includes('up') || className.includes('positive')) {
3449
+ textColor = '16A085';
3450
+ } else if (className.includes('down') || className.includes('negative')) {
3451
+ textColor = 'E74C3C';
3452
+ }
3453
+
3454
+ return {
3455
+ text: cellText,
3456
+ options: {
3457
+ ...textStyle,
3458
+ color: textColor,
3459
+ fill: fill,
3460
+ align: style.textAlign === 'center' ? 'center' : 'left',
3461
+ border: {
3462
+ type: 'none',
3463
+ bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
3464
+ }
3465
+ }
3466
+ };
3467
+ });
3468
+
3469
+ if (rowData.length > 0) {
3470
+ rows.push(rowData);
3471
+ }
3472
+ });
3473
+
3474
+ // Ensure all rows have same column count
3475
+ const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
3476
+ rows.forEach(row => {
3477
+ while (row.length < maxCols) {
3478
+ row.push({ text: '', options: {} });
3479
+ }
3480
+ });
3481
+ while (colWidths.length < maxCols) {
3482
+ colWidths.push(colWidths[colWidths.length - 1] || 1);
3483
+ }
3484
+
3485
+ return { rows, colWidths };
3486
+ }
3487
+
2734
3488
  exports.exportToPptx = exportToPptx;
2735
3489
  //# sourceMappingURL=dom-to-pptx.cjs.map