@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.
- package/.claude/settings.local.json +12 -0
- package/Readme.md +28 -0
- package/SUPPORTED.md +5 -0
- package/USAGE_CN.md +26 -0
- package/cli/dom-to-pptx.bundle.js +977 -36
- package/cli/html2pptx.js +19 -6
- package/cli/presentation.pptx +0 -0
- package/dist/dom-to-pptx.bundle.js +769 -14
- package/dist/dom-to-pptx.cjs +767 -13
- package/dist/dom-to-pptx.cjs.map +1 -1
- package/dist/dom-to-pptx.mjs +767 -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 +530 -10
- package/src/utils.js +240 -4
|
@@ -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
|
|
@@ -62520,6 +62521,7 @@
|
|
|
62520
62521
|
bold: textStyle.bold,
|
|
62521
62522
|
italic: textStyle.italic,
|
|
62522
62523
|
underline: textStyle.underline,
|
|
62524
|
+
lang: 'zh-CN',
|
|
62523
62525
|
|
|
62524
62526
|
fill: fill,
|
|
62525
62527
|
align: align,
|
|
@@ -62594,11 +62596,11 @@
|
|
|
62594
62596
|
// 3. Find first part that is a color (skip angle/direction)
|
|
62595
62597
|
for (const part of parts) {
|
|
62596
62598
|
// Ignore directions (to right) or angles (90deg, 0.5turn)
|
|
62597
|
-
if (/^(to\s|[\d
|
|
62599
|
+
if (/^(to\s|[\d.]+(deg|rad|turn|grad))/.test(part)) continue;
|
|
62598
62600
|
|
|
62599
62601
|
// Extract color: Remove trailing position (e.g. "red 50%" -> "red")
|
|
62600
62602
|
// Regex matches whitespace + number + unit at end of string
|
|
62601
|
-
const colorPart = part.replace(/\s+(-?[\d
|
|
62603
|
+
const colorPart = part.replace(/\s+(-?[\d.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
|
|
62602
62604
|
|
|
62603
62605
|
// Check if it's not just a number (some gradients might have bare numbers? unlikely in standard syntax)
|
|
62604
62606
|
if (colorPart) return colorPart;
|
|
@@ -62869,6 +62871,214 @@
|
|
|
62869
62871
|
return { hex, opacity: a };
|
|
62870
62872
|
}
|
|
62871
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
|
+
|
|
62872
63082
|
function getPadding(style, scale) {
|
|
62873
63083
|
const pxToInch = 1 / 96;
|
|
62874
63084
|
return [
|
|
@@ -62887,6 +63097,8 @@
|
|
|
62887
63097
|
}
|
|
62888
63098
|
|
|
62889
63099
|
const DEFAULT_CJK_FONTS = [
|
|
63100
|
+
'Heiti TC',
|
|
63101
|
+
'Heiti SC',
|
|
62890
63102
|
'PingFang SC',
|
|
62891
63103
|
'Hiragino Sans GB',
|
|
62892
63104
|
'Microsoft YaHei',
|
|
@@ -62938,7 +63150,7 @@
|
|
|
62938
63150
|
}
|
|
62939
63151
|
|
|
62940
63152
|
const autoMatch = DEFAULT_CJK_FONTS.find((f) => lowered.includes(f.toLowerCase()));
|
|
62941
|
-
return autoMatch ||
|
|
63153
|
+
return autoMatch || 'Heiti TC';
|
|
62942
63154
|
}
|
|
62943
63155
|
|
|
62944
63156
|
function getTextStyle(style, scale, text = '', options = {}) {
|
|
@@ -62969,6 +63181,9 @@
|
|
|
62969
63181
|
// And apply the global layout scale.
|
|
62970
63182
|
lineSpacing = lhPx * 0.75 * scale;
|
|
62971
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;
|
|
62972
63187
|
}
|
|
62973
63188
|
|
|
62974
63189
|
// --- Spacing (Margins) ---
|
|
@@ -62987,10 +63202,11 @@
|
|
|
62987
63202
|
return {
|
|
62988
63203
|
color: colorObj.hex || '000000',
|
|
62989
63204
|
fontFace: fontFace,
|
|
62990
|
-
fontSize: Math.floor(fontSizePx * 0.75 * scale),
|
|
63205
|
+
fontSize: Math.max(8, Math.floor(fontSizePx * 0.75 * scale)),
|
|
62991
63206
|
bold: parseInt(style.fontWeight) >= 600,
|
|
62992
63207
|
italic: style.fontStyle === 'italic',
|
|
62993
63208
|
underline: style.textDecoration.includes('underline'),
|
|
63209
|
+
lang: 'zh-CN',
|
|
62994
63210
|
// Only add if we have a valid value
|
|
62995
63211
|
...(lineSpacing && { lineSpacing }),
|
|
62996
63212
|
...(paraSpaceBefore > 0 && { paraSpaceBefore }),
|
|
@@ -63003,6 +63219,7 @@
|
|
|
63003
63219
|
/**
|
|
63004
63220
|
* Determines if a given DOM node is primarily a text container.
|
|
63005
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).
|
|
63006
63223
|
*/
|
|
63007
63224
|
function isTextContainer(node) {
|
|
63008
63225
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -63011,6 +63228,26 @@
|
|
|
63011
63228
|
const children = Array.from(node.children);
|
|
63012
63229
|
if (children.length === 0) return true;
|
|
63013
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
|
+
|
|
63014
63251
|
const isSafeInline = (el) => {
|
|
63015
63252
|
// 1. Reject Web Components / Custom Elements
|
|
63016
63253
|
if (el.tagName.includes('-')) return false;
|
|
@@ -63569,6 +63806,60 @@
|
|
|
63569
63806
|
const PPI = 96;
|
|
63570
63807
|
const PX_TO_INCH = 1 / PPI;
|
|
63571
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
|
+
|
|
63572
63863
|
/**
|
|
63573
63864
|
* Main export function.
|
|
63574
63865
|
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
@@ -63593,6 +63884,7 @@
|
|
|
63593
63884
|
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
63594
63885
|
const pptx = new PptxConstructor();
|
|
63595
63886
|
pptx.layout = 'LAYOUT_16x9';
|
|
63887
|
+
pptx.language = 'zh-CN';
|
|
63596
63888
|
|
|
63597
63889
|
const elements = Array.isArray(target) ? target : [target];
|
|
63598
63890
|
|
|
@@ -63667,6 +63959,9 @@
|
|
|
63667
63959
|
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
63668
63960
|
}
|
|
63669
63961
|
|
|
63962
|
+
// Fix Chinese font attributes (pitchFamily and charset) in all slide XMLs
|
|
63963
|
+
finalBlob = await fixChineseFontAttributes(finalBlob);
|
|
63964
|
+
|
|
63670
63965
|
// 4. Output Handling
|
|
63671
63966
|
// If skipDownload is NOT true, proceed with browser download
|
|
63672
63967
|
if (!options.skipDownload) {
|
|
@@ -63708,6 +64003,9 @@
|
|
|
63708
64003
|
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
63709
64004
|
};
|
|
63710
64005
|
|
|
64006
|
+
const chartRegistry = buildChartRegistry(root, globalOptions.chartConfigs);
|
|
64007
|
+
const slideOptions = { ...globalOptions, chartRegistry };
|
|
64008
|
+
|
|
63711
64009
|
const renderQueue = [];
|
|
63712
64010
|
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
63713
64011
|
let domOrderCounter = 0;
|
|
@@ -63743,7 +64041,7 @@
|
|
|
63743
64041
|
pptx,
|
|
63744
64042
|
currentZ,
|
|
63745
64043
|
nodeStyle,
|
|
63746
|
-
|
|
64044
|
+
slideOptions
|
|
63747
64045
|
);
|
|
63748
64046
|
|
|
63749
64047
|
if (result) {
|
|
@@ -63789,6 +64087,10 @@
|
|
|
63789
64087
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
63790
64088
|
if (item.type === 'image') slide.addImage(item.options);
|
|
63791
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
|
+
}
|
|
63792
64094
|
if (item.type === 'table') {
|
|
63793
64095
|
slide.addTable(item.tableData.rows, {
|
|
63794
64096
|
x: item.options.x,
|
|
@@ -63989,13 +64291,23 @@
|
|
|
63989
64291
|
range.detach();
|
|
63990
64292
|
|
|
63991
64293
|
const style = window.getComputedStyle(parent);
|
|
63992
|
-
|
|
63993
|
-
|
|
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
|
|
63994
64304
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
63995
64305
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
63996
64306
|
|
|
63997
|
-
|
|
63998
|
-
const
|
|
64307
|
+
// Use parent rect's position if we're using parent dimensions
|
|
64308
|
+
const sourceRect = useParentRect ? parentRect : rect;
|
|
64309
|
+
const x = config.offX + (sourceRect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
64310
|
+
const y = config.offY + (sourceRect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
63999
64311
|
|
|
64000
64312
|
return {
|
|
64001
64313
|
items: [
|
|
@@ -64034,7 +64346,7 @@
|
|
|
64034
64346
|
const elementOpacity = parseFloat(style.opacity);
|
|
64035
64347
|
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
64036
64348
|
|
|
64037
|
-
|
|
64349
|
+
let widthPx = node.offsetWidth || rect.width;
|
|
64038
64350
|
const heightPx = node.offsetHeight || rect.height;
|
|
64039
64351
|
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
64040
64352
|
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
@@ -64048,8 +64360,41 @@
|
|
|
64048
64360
|
|
|
64049
64361
|
const items = [];
|
|
64050
64362
|
|
|
64051
|
-
|
|
64052
|
-
|
|
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
|
+
}
|
|
64053
64398
|
const cellBgItems = [];
|
|
64054
64399
|
const renderCellBg = globalOptions.tableConfig?.renderCellBackgrounds !== false;
|
|
64055
64400
|
|
|
@@ -64393,6 +64738,35 @@
|
|
|
64393
64738
|
return { items: [item], job, stopRecursion: true };
|
|
64394
64739
|
}
|
|
64395
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
|
+
|
|
64396
64770
|
// Radii logic
|
|
64397
64771
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
64398
64772
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
@@ -64495,6 +64869,27 @@
|
|
|
64495
64869
|
let textPayload = null;
|
|
64496
64870
|
const isText = isTextContainer(node);
|
|
64497
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
|
+
|
|
64498
64893
|
if (isText) {
|
|
64499
64894
|
const textParts = [];
|
|
64500
64895
|
let trimNextLeading = false;
|
|
@@ -64551,8 +64946,91 @@
|
|
|
64551
64946
|
let align = style.textAlign || 'left';
|
|
64552
64947
|
if (align === 'start') align = 'left';
|
|
64553
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) || hasGradient;
|
|
64967
|
+
const hasVisibleBorder = borderWidth > 0;
|
|
64968
|
+
const hasLargeBorderRadius = borderRadius >= height / 2;
|
|
64969
|
+
const hasBadgeClass = className.includes('badge') || className.includes('pill') || className.includes('tag');
|
|
64970
|
+
|
|
64971
|
+
// Check if it's a small tag/label with rounded corners and short text
|
|
64972
|
+
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
64973
|
+
const paddingRight = parseFloat(style.paddingRight) || 0;
|
|
64974
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
64975
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
64976
|
+
const hasSmallPadding = Math.max(paddingLeft, paddingRight, paddingTop, paddingBottom) <= 12;
|
|
64977
|
+
const hasEvenPadding = Math.abs(paddingTop - paddingBottom) <= 4 && Math.abs(paddingLeft - paddingRight) <= 8;
|
|
64978
|
+
const hasRoundedCorners = borderRadius >= 3;
|
|
64979
|
+
const isInlineBlock = style.display === 'inline-block' || style.display === 'inline-flex';
|
|
64980
|
+
|
|
64981
|
+
// Small tag detection: inline-block with background, rounded corners, small even padding, and short text
|
|
64982
|
+
const isSmallTag =
|
|
64983
|
+
isInlineBlock &&
|
|
64984
|
+
hasVisibleBackground &&
|
|
64985
|
+
hasRoundedCorners &&
|
|
64986
|
+
hasSmallPadding &&
|
|
64987
|
+
hasEvenPadding &&
|
|
64988
|
+
textContent.length <= 10;
|
|
64989
|
+
|
|
64990
|
+
// Only consider it a badge if it has visual styling AND short text
|
|
64991
|
+
const isLikelyBadge =
|
|
64992
|
+
((hasLargeBorderRadius || hasBadgeClass) &&
|
|
64993
|
+
textContent.length <= 10 &&
|
|
64994
|
+
(hasVisibleBackground || hasVisibleBorder || hasLargeBorderRadius)) ||
|
|
64995
|
+
isSmallTag;
|
|
64996
|
+
|
|
64997
|
+
if (isLikelyBadge) {
|
|
64998
|
+
align = 'center';
|
|
64999
|
+
}
|
|
65000
|
+
|
|
64554
65001
|
let valign = 'top';
|
|
64555
|
-
|
|
65002
|
+
|
|
65003
|
+
// For flex items, valign should be determined by PARENT's align-items, not self
|
|
65004
|
+
// Self's align-items controls how children are aligned, not self's position in parent
|
|
65005
|
+
const parentEl = node.parentElement;
|
|
65006
|
+
if (parentEl) {
|
|
65007
|
+
const parentStyle = window.getComputedStyle(parentEl);
|
|
65008
|
+
if (parentStyle.display.includes('flex')) {
|
|
65009
|
+
// Parent's align-items controls this element's cross-axis alignment
|
|
65010
|
+
if (parentStyle.alignItems === 'center') {
|
|
65011
|
+
valign = 'middle';
|
|
65012
|
+
} else if (parentStyle.alignItems === 'flex-end' || parentStyle.alignItems === 'end') {
|
|
65013
|
+
valign = 'bottom';
|
|
65014
|
+
}
|
|
65015
|
+
// Default (stretch, flex-start) keeps valign = 'top'
|
|
65016
|
+
}
|
|
65017
|
+
}
|
|
65018
|
+
|
|
65019
|
+
// If element is not a flex item (no flex parent), then its own align-items
|
|
65020
|
+
// might indicate self-centering intent for single-element containers
|
|
65021
|
+
if (valign === 'top' && style.alignItems === 'center' && !parentEl) {
|
|
65022
|
+
valign = 'middle';
|
|
65023
|
+
}
|
|
65024
|
+
|
|
65025
|
+
// Auto-center vertically for likely badges
|
|
65026
|
+
if (isLikelyBadge && valign !== 'middle') {
|
|
65027
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
65028
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
65029
|
+
// If padding is relatively even, assume vertical centering is intended
|
|
65030
|
+
if (Math.abs(paddingTop - paddingBottom) <= 4) {
|
|
65031
|
+
valign = 'middle';
|
|
65032
|
+
}
|
|
65033
|
+
}
|
|
64556
65034
|
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
64557
65035
|
|
|
64558
65036
|
const pt = parseFloat(style.paddingTop) || 0;
|
|
@@ -64912,6 +65390,283 @@
|
|
|
64912
65390
|
return items;
|
|
64913
65391
|
}
|
|
64914
65392
|
|
|
65393
|
+
/**
|
|
65394
|
+
* Detect absolutely positioned children that overflow their parent
|
|
65395
|
+
* This handles cases like chart value labels positioned above bars
|
|
65396
|
+
*/
|
|
65397
|
+
function detectOverflowingChildren(node) {
|
|
65398
|
+
const overflowing = [];
|
|
65399
|
+
const parentRect = node.getBoundingClientRect();
|
|
65400
|
+
|
|
65401
|
+
// Only check for elements with specific class patterns that suggest chart values
|
|
65402
|
+
const children = node.querySelectorAll('.bar-value, [class*="value"], [class*="label"]');
|
|
65403
|
+
|
|
65404
|
+
children.forEach(child => {
|
|
65405
|
+
const childStyle = window.getComputedStyle(child);
|
|
65406
|
+
const childRect = child.getBoundingClientRect();
|
|
65407
|
+
|
|
65408
|
+
// Check if absolutely positioned and outside parent bounds
|
|
65409
|
+
if (childStyle.position === 'absolute') {
|
|
65410
|
+
const isOutside =
|
|
65411
|
+
childRect.bottom < parentRect.top ||
|
|
65412
|
+
childRect.top > parentRect.bottom ||
|
|
65413
|
+
childRect.right < parentRect.left ||
|
|
65414
|
+
childRect.left > parentRect.right;
|
|
65415
|
+
|
|
65416
|
+
if (isOutside || childRect.top < parentRect.top) {
|
|
65417
|
+
overflowing.push(child);
|
|
65418
|
+
}
|
|
65419
|
+
}
|
|
65420
|
+
});
|
|
65421
|
+
|
|
65422
|
+
return overflowing;
|
|
65423
|
+
}
|
|
65424
|
+
|
|
65425
|
+
/**
|
|
65426
|
+
* Process content with overflowing children
|
|
65427
|
+
* Captures the entire visual area including overflowing elements
|
|
65428
|
+
*/
|
|
65429
|
+
function processOverflowingContent(node, overflowingChildren, config, domOrder, pptx, zIndex, style, globalOptions) {
|
|
65430
|
+
// Calculate bounding box that includes all overflowing children
|
|
65431
|
+
const rect = node.getBoundingClientRect();
|
|
65432
|
+
let minX = rect.left;
|
|
65433
|
+
let minY = rect.top;
|
|
65434
|
+
let maxX = rect.right;
|
|
65435
|
+
let maxY = rect.bottom;
|
|
65436
|
+
|
|
65437
|
+
overflowingChildren.forEach(child => {
|
|
65438
|
+
const childRect = child.getBoundingClientRect();
|
|
65439
|
+
minX = Math.min(minX, childRect.left);
|
|
65440
|
+
minY = Math.min(minY, childRect.top);
|
|
65441
|
+
maxX = Math.max(maxX, childRect.right);
|
|
65442
|
+
maxY = Math.max(maxY, childRect.bottom);
|
|
65443
|
+
});
|
|
65444
|
+
|
|
65445
|
+
const totalWidth = maxX - minX;
|
|
65446
|
+
const totalHeight = maxY - minY;
|
|
65447
|
+
|
|
65448
|
+
// Use html2canvas to capture the entire area
|
|
65449
|
+
const item = {
|
|
65450
|
+
type: 'image',
|
|
65451
|
+
zIndex,
|
|
65452
|
+
domOrder,
|
|
65453
|
+
options: {
|
|
65454
|
+
x: config.offX + (minX - config.rootX) * PX_TO_INCH * config.scale,
|
|
65455
|
+
y: config.offY + (minY - config.rootY) * PX_TO_INCH * config.scale,
|
|
65456
|
+
w: totalWidth * PX_TO_INCH * config.scale,
|
|
65457
|
+
h: totalHeight * PX_TO_INCH * config.scale,
|
|
65458
|
+
data: null
|
|
65459
|
+
}
|
|
65460
|
+
};
|
|
65461
|
+
|
|
65462
|
+
const job = async () => {
|
|
65463
|
+
try {
|
|
65464
|
+
// Temporarily adjust node position for capture
|
|
65465
|
+
const originalPosition = node.style.position;
|
|
65466
|
+
const originalTransform = node.style.transform;
|
|
65467
|
+
|
|
65468
|
+
node.style.position = 'relative';
|
|
65469
|
+
node.style.transform = 'none';
|
|
65470
|
+
|
|
65471
|
+
const canvas = await html2canvas(node, {
|
|
65472
|
+
backgroundColor: null,
|
|
65473
|
+
logging: false,
|
|
65474
|
+
scale: 2,
|
|
65475
|
+
useCORS: true,
|
|
65476
|
+
x: minX - rect.left,
|
|
65477
|
+
y: minY - rect.top,
|
|
65478
|
+
width: totalWidth,
|
|
65479
|
+
height: totalHeight
|
|
65480
|
+
});
|
|
65481
|
+
|
|
65482
|
+
// Restore original styles
|
|
65483
|
+
node.style.position = originalPosition;
|
|
65484
|
+
node.style.transform = originalTransform;
|
|
65485
|
+
|
|
65486
|
+
item.options.data = canvas.toDataURL('image/png');
|
|
65487
|
+
} catch (e) {
|
|
65488
|
+
console.warn('Failed to capture overflowing content:', e);
|
|
65489
|
+
item.skip = true;
|
|
65490
|
+
}
|
|
65491
|
+
};
|
|
65492
|
+
|
|
65493
|
+
return { items: [item], job, stopRecursion: true };
|
|
65494
|
+
}
|
|
65495
|
+
|
|
65496
|
+
/**
|
|
65497
|
+
* Detect vertical stat cards (like .mini-stat with number above label)
|
|
65498
|
+
* These have block-level children that should stack vertically
|
|
65499
|
+
*/
|
|
65500
|
+
function isVerticalStatCard(node) {
|
|
65501
|
+
const className = (node.className || '').toLowerCase();
|
|
65502
|
+
|
|
65503
|
+
// Check for stat-like class names
|
|
65504
|
+
const statIndicators = ['stat', 'metric', 'kpi', 'data-item', 'stat-item'];
|
|
65505
|
+
const hasStatClass = statIndicators.some(indicator => className.includes(indicator));
|
|
65506
|
+
|
|
65507
|
+
if (!hasStatClass) return false;
|
|
65508
|
+
|
|
65509
|
+
const children = Array.from(node.children);
|
|
65510
|
+
if (children.length !== 2) return false;
|
|
65511
|
+
|
|
65512
|
+
// Check if children are likely number + label pair
|
|
65513
|
+
const child1 = children[0];
|
|
65514
|
+
const child2 = children[1];
|
|
65515
|
+
|
|
65516
|
+
const style1 = window.getComputedStyle(child1);
|
|
65517
|
+
const style2 = window.getComputedStyle(child2);
|
|
65518
|
+
|
|
65519
|
+
// Both should be block elements (or have block-like display)
|
|
65520
|
+
const isBlock1 = style1.display === 'block' || style1.display === 'flex';
|
|
65521
|
+
const isBlock2 = style2.display === 'block' || style2.display === 'flex';
|
|
65522
|
+
|
|
65523
|
+
if (!isBlock1 || !isBlock2) return false;
|
|
65524
|
+
|
|
65525
|
+
// First child should have larger font (the number)
|
|
65526
|
+
const fontSize1 = parseFloat(style1.fontSize) || 0;
|
|
65527
|
+
const fontSize2 = parseFloat(style2.fontSize) || 0;
|
|
65528
|
+
|
|
65529
|
+
// Number should be larger than label, or at least bold
|
|
65530
|
+
const isBold1 = parseInt(style1.fontWeight) >= 600;
|
|
65531
|
+
|
|
65532
|
+
return fontSize1 >= fontSize2 || isBold1;
|
|
65533
|
+
}
|
|
65534
|
+
|
|
65535
|
+
/**
|
|
65536
|
+
* Detect if a div structure looks like a table
|
|
65537
|
+
* Checks for table-like class names and structure patterns
|
|
65538
|
+
*/
|
|
65539
|
+
function detectTableLikeStructure(node) {
|
|
65540
|
+
const className = (node.className || '').toLowerCase();
|
|
65541
|
+
|
|
65542
|
+
// Check for table-related class names
|
|
65543
|
+
const tableIndicators = ['table', 'data-table', 'grid', 'list'];
|
|
65544
|
+
const hasTableClass = tableIndicators.some(indicator => className.includes(indicator));
|
|
65545
|
+
|
|
65546
|
+
if (!hasTableClass) return false;
|
|
65547
|
+
|
|
65548
|
+
// Check structure: should have header row and data rows
|
|
65549
|
+
const children = Array.from(node.children);
|
|
65550
|
+
|
|
65551
|
+
// Look for header-like element
|
|
65552
|
+
const hasHeader = children.some(child =>
|
|
65553
|
+
(child.className || '').toLowerCase().includes('header') ||
|
|
65554
|
+
child.tagName === 'THEAD'
|
|
65555
|
+
);
|
|
65556
|
+
|
|
65557
|
+
// Look for row-like elements
|
|
65558
|
+
const hasRows = children.some(child =>
|
|
65559
|
+
(child.className || '').toLowerCase().includes('row') ||
|
|
65560
|
+
child.tagName === 'TR' ||
|
|
65561
|
+
child.tagName === 'TBODY'
|
|
65562
|
+
);
|
|
65563
|
+
|
|
65564
|
+
// Check for cell-like structure in children
|
|
65565
|
+
const hasCellStructure = children.some(child => {
|
|
65566
|
+
const childChildren = Array.from(child.children);
|
|
65567
|
+
return childChildren.some(grandchild =>
|
|
65568
|
+
(grandchild.className || '').toLowerCase().includes('cell') ||
|
|
65569
|
+
grandchild.className.toLowerCase().includes('col') ||
|
|
65570
|
+
grandchild.className.toLowerCase().includes('td')
|
|
65571
|
+
);
|
|
65572
|
+
});
|
|
65573
|
+
|
|
65574
|
+
return (hasHeader || hasRows) && hasCellStructure;
|
|
65575
|
+
}
|
|
65576
|
+
|
|
65577
|
+
/**
|
|
65578
|
+
* Extract table data from div-based table structure
|
|
65579
|
+
* Similar to extractTableData but for div grids
|
|
65580
|
+
*/
|
|
65581
|
+
function extractDivTableData(node, scale, options = {}) {
|
|
65582
|
+
const rows = [];
|
|
65583
|
+
const colWidths = [];
|
|
65584
|
+
const root = options.root || null;
|
|
65585
|
+
|
|
65586
|
+
// Find header row
|
|
65587
|
+
const headerRow = node.querySelector('.table-header, [class*="header"]');
|
|
65588
|
+
if (headerRow) {
|
|
65589
|
+
const headerCells = Array.from(headerRow.children);
|
|
65590
|
+
headerCells.forEach((cell, index) => {
|
|
65591
|
+
const rect = cell.getBoundingClientRect();
|
|
65592
|
+
const wIn = rect.width * (1 / 96) * scale;
|
|
65593
|
+
colWidths[index] = wIn;
|
|
65594
|
+
});
|
|
65595
|
+
|
|
65596
|
+
// Process header as first row
|
|
65597
|
+
const headerData = headerCells.map(cell => {
|
|
65598
|
+
const style = window.getComputedStyle(cell);
|
|
65599
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
65600
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
65601
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
65602
|
+
|
|
65603
|
+
return {
|
|
65604
|
+
text: cellText,
|
|
65605
|
+
options: {
|
|
65606
|
+
...textStyle,
|
|
65607
|
+
fill: fill || { color: '16A085', transparency: 80 },
|
|
65608
|
+
bold: true,
|
|
65609
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
65610
|
+
border: { type: 'none' }
|
|
65611
|
+
}
|
|
65612
|
+
};
|
|
65613
|
+
});
|
|
65614
|
+
rows.push(headerData);
|
|
65615
|
+
}
|
|
65616
|
+
|
|
65617
|
+
// Find data rows
|
|
65618
|
+
const dataRows = node.querySelectorAll('.table-row, [class*="row"]');
|
|
65619
|
+
dataRows.forEach(row => {
|
|
65620
|
+
const cells = Array.from(row.children);
|
|
65621
|
+
const rowData = cells.map(cell => {
|
|
65622
|
+
const style = window.getComputedStyle(cell);
|
|
65623
|
+
const cellText = cell.innerText.replace(/[\n\r\t]+/g, ' ').trim();
|
|
65624
|
+
const textStyle = getTextStyle(style, scale, cellText, options);
|
|
65625
|
+
const fill = computeTableCellFill(style, cell, root, options);
|
|
65626
|
+
|
|
65627
|
+
// Detect trend colors
|
|
65628
|
+
const className = (cell.className || '').toLowerCase();
|
|
65629
|
+
let textColor = textStyle.color;
|
|
65630
|
+
if (className.includes('up') || className.includes('positive')) {
|
|
65631
|
+
textColor = '16A085';
|
|
65632
|
+
} else if (className.includes('down') || className.includes('negative')) {
|
|
65633
|
+
textColor = 'E74C3C';
|
|
65634
|
+
}
|
|
65635
|
+
|
|
65636
|
+
return {
|
|
65637
|
+
text: cellText,
|
|
65638
|
+
options: {
|
|
65639
|
+
...textStyle,
|
|
65640
|
+
color: textColor,
|
|
65641
|
+
fill: fill,
|
|
65642
|
+
align: style.textAlign === 'center' ? 'center' : 'left',
|
|
65643
|
+
border: {
|
|
65644
|
+
type: 'none',
|
|
65645
|
+
bottom: { type: 'solid', pt: 0.5, color: 'FFFFFF', transparency: 95 }
|
|
65646
|
+
}
|
|
65647
|
+
}
|
|
65648
|
+
};
|
|
65649
|
+
});
|
|
65650
|
+
|
|
65651
|
+
if (rowData.length > 0) {
|
|
65652
|
+
rows.push(rowData);
|
|
65653
|
+
}
|
|
65654
|
+
});
|
|
65655
|
+
|
|
65656
|
+
// Ensure all rows have same column count
|
|
65657
|
+
const maxCols = Math.max(...rows.map(r => r.length), colWidths.length);
|
|
65658
|
+
rows.forEach(row => {
|
|
65659
|
+
while (row.length < maxCols) {
|
|
65660
|
+
row.push({ text: '', options: {} });
|
|
65661
|
+
}
|
|
65662
|
+
});
|
|
65663
|
+
while (colWidths.length < maxCols) {
|
|
65664
|
+
colWidths.push(colWidths[colWidths.length - 1] || 1);
|
|
65665
|
+
}
|
|
65666
|
+
|
|
65667
|
+
return { rows, colWidths };
|
|
65668
|
+
}
|
|
65669
|
+
|
|
64915
65670
|
exports.exportToPptx = exportToPptx;
|
|
64916
65671
|
|
|
64917
65672
|
}));
|