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