@microlee666/dom-to-pptx 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/Readme.md +28 -0
- package/SUPPORTED.md +5 -0
- package/USAGE_CN.md +26 -0
- package/cli/dom-to-pptx.bundle.js +957 -36
- package/cli/html2pptx.js +19 -6
- package/cli/presentation.pptx +0 -0
- package/dist/dom-to-pptx.bundle.js +749 -14
- package/dist/dom-to-pptx.cjs +747 -13
- package/dist/dom-to-pptx.cjs.map +1 -1
- package/dist/dom-to-pptx.mjs +747 -13
- package/dist/dom-to-pptx.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/patch-bundle.js +66 -0
- package/src/index.js +510 -10
- package/src/utils.js +240 -4
package/src/utils.js
CHANGED
|
@@ -109,6 +109,7 @@ export function extractTableData(node, scale, options = {}) {
|
|
|
109
109
|
bold: textStyle.bold,
|
|
110
110
|
italic: textStyle.italic,
|
|
111
111
|
underline: textStyle.underline,
|
|
112
|
+
lang: 'zh-CN',
|
|
112
113
|
|
|
113
114
|
fill: fill,
|
|
114
115
|
align: align,
|
|
@@ -183,11 +184,11 @@ export function getGradientFallbackColor(bgImage) {
|
|
|
183
184
|
// 3. Find first part that is a color (skip angle/direction)
|
|
184
185
|
for (const part of parts) {
|
|
185
186
|
// Ignore directions (to right) or angles (90deg, 0.5turn)
|
|
186
|
-
if (/^(to\s|[\d
|
|
187
|
+
if (/^(to\s|[\d.]+(deg|rad|turn|grad))/.test(part)) continue;
|
|
187
188
|
|
|
188
189
|
// Extract color: Remove trailing position (e.g. "red 50%" -> "red")
|
|
189
190
|
// Regex matches whitespace + number + unit at end of string
|
|
190
|
-
const colorPart = part.replace(/\s+(-?[\d
|
|
191
|
+
const colorPart = part.replace(/\s+(-?[\d.]+(%|px|em|rem|ch|vh|vw)?)$/, '');
|
|
191
192
|
|
|
192
193
|
// Check if it's not just a number (some gradients might have bare numbers? unlikely in standard syntax)
|
|
193
194
|
if (colorPart) return colorPart;
|
|
@@ -458,6 +459,214 @@ export function parseColor(str) {
|
|
|
458
459
|
return { hex, opacity: a };
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
const SUPPORTED_CHART_TYPES = new Set(['area', 'bar', 'line', 'pie', 'doughnut', 'radar', 'scatter']);
|
|
463
|
+
const CHART_TYPE_ALIASES = {
|
|
464
|
+
column: 'bar',
|
|
465
|
+
columnbar: 'bar',
|
|
466
|
+
column3d: 'bar',
|
|
467
|
+
bar3d: 'bar',
|
|
468
|
+
donut: 'doughnut',
|
|
469
|
+
ring: 'doughnut',
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
function normalizeLegendPos(value) {
|
|
473
|
+
if (!value) return null;
|
|
474
|
+
const normalized = value.toString().toLowerCase().replace(/[^a-z]/g, '');
|
|
475
|
+
if (normalized === 'tr' || normalized === 'topright') return 'tr';
|
|
476
|
+
if (normalized === 'top') return 't';
|
|
477
|
+
if (normalized === 'bottom') return 'b';
|
|
478
|
+
if (normalized === 'left') return 'l';
|
|
479
|
+
if (normalized === 'right') return 'r';
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function normalizeChartType(value) {
|
|
484
|
+
if (!value) return null;
|
|
485
|
+
const raw = value.toString().trim().toLowerCase();
|
|
486
|
+
const alias = CHART_TYPE_ALIASES[raw];
|
|
487
|
+
const candidate = alias || raw;
|
|
488
|
+
return SUPPORTED_CHART_TYPES.has(candidate) ? candidate : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function colorToHex(value) {
|
|
492
|
+
const parsed = parseColor(value);
|
|
493
|
+
return parsed.hex || null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function resolveChartElement(root, config) {
|
|
497
|
+
if (!config) return null;
|
|
498
|
+
if (config.element instanceof HTMLElement) return config.element;
|
|
499
|
+
if (typeof config.element === 'function') return config.element(root);
|
|
500
|
+
if (typeof config.selector === 'string') {
|
|
501
|
+
return root.querySelector(config.selector);
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function buildSeriesFromSource(source = {}) {
|
|
507
|
+
const fallbackLabels = Array.isArray(source.labels) ? source.labels : [];
|
|
508
|
+
const candidateDatasets = Array.isArray(source.datasets) ? source.datasets : [];
|
|
509
|
+
const series = [];
|
|
510
|
+
|
|
511
|
+
candidateDatasets.forEach((dataset, index) => {
|
|
512
|
+
if (!dataset) return;
|
|
513
|
+
const values = Array.isArray(dataset.values)
|
|
514
|
+
? dataset.values
|
|
515
|
+
: Array.isArray(dataset.data)
|
|
516
|
+
? dataset.data
|
|
517
|
+
: [];
|
|
518
|
+
|
|
519
|
+
if (!values.length) return;
|
|
520
|
+
|
|
521
|
+
const record = {
|
|
522
|
+
name: dataset.name || dataset.label || `Series ${index + 1}`,
|
|
523
|
+
values,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
if (Array.isArray(dataset.labels) && dataset.labels.length === values.length) {
|
|
527
|
+
record.labels = dataset.labels;
|
|
528
|
+
} else if (fallbackLabels.length === values.length) {
|
|
529
|
+
record.labels = fallbackLabels;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (Array.isArray(dataset.sizes) && dataset.sizes.length === values.length) {
|
|
533
|
+
record.sizes = dataset.sizes;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Chart.js often provides strings/arrays in backgroundColor
|
|
537
|
+
const candidateColor =
|
|
538
|
+
dataset.color || dataset.backgroundColor || dataset.borderColor || dataset.fillColor;
|
|
539
|
+
if (candidateColor) {
|
|
540
|
+
if (Array.isArray(candidateColor)) {
|
|
541
|
+
record.color = candidateColor[0];
|
|
542
|
+
} else {
|
|
543
|
+
record.color = candidateColor;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
series.push(record);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (!series.length) {
|
|
551
|
+
const fallbackValues = Array.isArray(source.values)
|
|
552
|
+
? source.values
|
|
553
|
+
: Array.isArray(source.data)
|
|
554
|
+
? source.data
|
|
555
|
+
: [];
|
|
556
|
+
|
|
557
|
+
if (fallbackValues.length) {
|
|
558
|
+
const fallbackRecord = {
|
|
559
|
+
name: source.name || source.label || 'Series 1',
|
|
560
|
+
values: fallbackValues,
|
|
561
|
+
};
|
|
562
|
+
if (Array.isArray(source.labels) && source.labels.length === fallbackValues.length) {
|
|
563
|
+
fallbackRecord.labels = source.labels;
|
|
564
|
+
} else if (fallbackLabels.length === fallbackValues.length) {
|
|
565
|
+
fallbackRecord.labels = fallbackLabels;
|
|
566
|
+
}
|
|
567
|
+
if (Array.isArray(source.sizes) && source.sizes.length === fallbackValues.length) {
|
|
568
|
+
fallbackRecord.sizes = source.sizes;
|
|
569
|
+
}
|
|
570
|
+
if (source.color) {
|
|
571
|
+
fallbackRecord.color = source.color;
|
|
572
|
+
}
|
|
573
|
+
series.push(fallbackRecord);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return series;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function buildChartOptions(raw, derivedColors) {
|
|
581
|
+
const opts = { ...((raw && raw.chartOptions) || {}) };
|
|
582
|
+
if (raw && raw.showLegend !== undefined) opts.showLegend = raw.showLegend;
|
|
583
|
+
if (raw) {
|
|
584
|
+
const legendTarget = raw.legendPos || raw.legendPosition;
|
|
585
|
+
const normalizedLegend = normalizeLegendPos(legendTarget);
|
|
586
|
+
if (normalizedLegend) opts.legendPos = normalizedLegend;
|
|
587
|
+
}
|
|
588
|
+
if (raw && raw.title) opts.title = raw.title;
|
|
589
|
+
if (raw && raw.altText) opts.altText = raw.altText;
|
|
590
|
+
if (raw && raw.showDataTable !== undefined) opts.showDataTable = raw.showDataTable;
|
|
591
|
+
if (raw && raw.chartColorsOpacity !== undefined) opts.chartColorsOpacity = raw.chartColorsOpacity;
|
|
592
|
+
if (raw && raw.showLabel !== undefined) opts.showLabel = raw.showLabel;
|
|
593
|
+
|
|
594
|
+
const paletteFromConfig =
|
|
595
|
+
raw && Array.isArray(raw.chartColors) ? raw.chartColors.map(colorToHex).filter(Boolean) : [];
|
|
596
|
+
const palette = paletteFromConfig.length ? paletteFromConfig : derivedColors;
|
|
597
|
+
if (palette.length && !opts.chartColors) {
|
|
598
|
+
opts.chartColors = palette;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return opts;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function normalizeChartConfig(raw) {
|
|
605
|
+
if (!raw) return null;
|
|
606
|
+
const chartType = normalizeChartType(raw.chartType || raw.type || raw.chart);
|
|
607
|
+
if (!chartType) return null;
|
|
608
|
+
|
|
609
|
+
let seriesWithColor = [];
|
|
610
|
+
if (Array.isArray(raw.chartData) && raw.chartData.length) {
|
|
611
|
+
seriesWithColor = raw.chartData
|
|
612
|
+
.map((entry, index) => {
|
|
613
|
+
if (!entry || !Array.isArray(entry.values)) return null;
|
|
614
|
+
return {
|
|
615
|
+
...entry,
|
|
616
|
+
name: entry.name || entry.label || `Series ${index + 1}`,
|
|
617
|
+
};
|
|
618
|
+
})
|
|
619
|
+
.filter((entry) => entry && entry.values && entry.values.length);
|
|
620
|
+
} else {
|
|
621
|
+
const source = {
|
|
622
|
+
labels: raw.data?.labels || raw.labels,
|
|
623
|
+
datasets: Array.isArray(raw.data?.datasets)
|
|
624
|
+
? raw.data.datasets
|
|
625
|
+
: Array.isArray(raw.datasets)
|
|
626
|
+
? raw.datasets
|
|
627
|
+
: [],
|
|
628
|
+
values: raw.data?.values || raw.values,
|
|
629
|
+
data: raw.data?.data,
|
|
630
|
+
name: raw.name,
|
|
631
|
+
label: raw.label,
|
|
632
|
+
color: raw.color,
|
|
633
|
+
sizes: raw.data?.sizes || raw.sizes,
|
|
634
|
+
};
|
|
635
|
+
seriesWithColor = buildSeriesFromSource(source);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!seriesWithColor.length) return null;
|
|
639
|
+
|
|
640
|
+
const derivedColors = seriesWithColor
|
|
641
|
+
.map((dataset) => dataset.color)
|
|
642
|
+
.map(colorToHex)
|
|
643
|
+
.filter(Boolean);
|
|
644
|
+
|
|
645
|
+
const chartOptions = buildChartOptions(raw, derivedColors);
|
|
646
|
+
const chartData = seriesWithColor.map(({ color, ...rest }) => rest);
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
chartType,
|
|
650
|
+
chartData,
|
|
651
|
+
chartOptions,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function buildChartRegistry(root, chartConfigs = []) {
|
|
656
|
+
const registry = new Map();
|
|
657
|
+
if (!root || !Array.isArray(chartConfigs)) return registry;
|
|
658
|
+
|
|
659
|
+
chartConfigs.forEach((config) => {
|
|
660
|
+
const node = resolveChartElement(root, config);
|
|
661
|
+
if (!node) return;
|
|
662
|
+
const normalized = normalizeChartConfig(config);
|
|
663
|
+
if (!normalized || !normalized.chartData || !normalized.chartData.length) return;
|
|
664
|
+
registry.set(node, normalized);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return registry;
|
|
668
|
+
}
|
|
669
|
+
|
|
461
670
|
export function getPadding(style, scale) {
|
|
462
671
|
const pxToInch = 1 / 96;
|
|
463
672
|
return [
|
|
@@ -476,6 +685,8 @@ export function getSoftEdges(filterStr, scale) {
|
|
|
476
685
|
}
|
|
477
686
|
|
|
478
687
|
const DEFAULT_CJK_FONTS = [
|
|
688
|
+
'Heiti TC',
|
|
689
|
+
'Heiti SC',
|
|
479
690
|
'PingFang SC',
|
|
480
691
|
'Hiragino Sans GB',
|
|
481
692
|
'Microsoft YaHei',
|
|
@@ -527,7 +738,7 @@ function pickFontFace(fontFamily, text, options) {
|
|
|
527
738
|
}
|
|
528
739
|
|
|
529
740
|
const autoMatch = DEFAULT_CJK_FONTS.find((f) => lowered.includes(f.toLowerCase()));
|
|
530
|
-
return autoMatch ||
|
|
741
|
+
return autoMatch || 'Heiti TC';
|
|
531
742
|
}
|
|
532
743
|
|
|
533
744
|
export function getTextStyle(style, scale, text = '', options = {}) {
|
|
@@ -558,6 +769,9 @@ export function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
558
769
|
// And apply the global layout scale.
|
|
559
770
|
lineSpacing = lhPx * 0.75 * scale;
|
|
560
771
|
}
|
|
772
|
+
} else {
|
|
773
|
+
// Default line height when 'normal' - use 1.2 multiplier to match browser default
|
|
774
|
+
lineSpacing = fontSizePx * 1.2 * 0.75 * scale;
|
|
561
775
|
}
|
|
562
776
|
|
|
563
777
|
// --- Spacing (Margins) ---
|
|
@@ -576,10 +790,11 @@ export function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
576
790
|
return {
|
|
577
791
|
color: colorObj.hex || '000000',
|
|
578
792
|
fontFace: fontFace,
|
|
579
|
-
fontSize: Math.floor(fontSizePx * 0.75 * scale),
|
|
793
|
+
fontSize: Math.max(8, Math.floor(fontSizePx * 0.75 * scale)),
|
|
580
794
|
bold: parseInt(style.fontWeight) >= 600,
|
|
581
795
|
italic: style.fontStyle === 'italic',
|
|
582
796
|
underline: style.textDecoration.includes('underline'),
|
|
797
|
+
lang: 'zh-CN',
|
|
583
798
|
// Only add if we have a valid value
|
|
584
799
|
...(lineSpacing && { lineSpacing }),
|
|
585
800
|
...(paraSpaceBefore > 0 && { paraSpaceBefore }),
|
|
@@ -592,6 +807,7 @@ export function getTextStyle(style, scale, text = '', options = {}) {
|
|
|
592
807
|
/**
|
|
593
808
|
* Determines if a given DOM node is primarily a text container.
|
|
594
809
|
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
810
|
+
* Also rejects flex/grid containers with distributed children (space-between/around/evenly).
|
|
595
811
|
*/
|
|
596
812
|
export function isTextContainer(node) {
|
|
597
813
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -600,6 +816,26 @@ export function isTextContainer(node) {
|
|
|
600
816
|
const children = Array.from(node.children);
|
|
601
817
|
if (children.length === 0) return true;
|
|
602
818
|
|
|
819
|
+
// Check if parent is a flex/grid container with special layout
|
|
820
|
+
// In such cases, children should be treated as separate elements
|
|
821
|
+
const parentStyle = window.getComputedStyle(node);
|
|
822
|
+
const display = parentStyle.display;
|
|
823
|
+
const justifyContent = parentStyle.justifyContent || '';
|
|
824
|
+
const alignItems = parentStyle.alignItems || '';
|
|
825
|
+
|
|
826
|
+
// If parent uses flex/grid with space distribution, don't treat as text container
|
|
827
|
+
if ((display.includes('flex') || display.includes('grid')) &&
|
|
828
|
+
(justifyContent.includes('space-between') ||
|
|
829
|
+
justifyContent.includes('space-around') ||
|
|
830
|
+
justifyContent.includes('space-evenly'))) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Note: We don't skip text container for align-items: center here.
|
|
835
|
+
// The valign inheritance is handled in prepareRenderItem with single-child check.
|
|
836
|
+
// This allows elements like "密钥状态流转" to properly inherit center alignment
|
|
837
|
+
// while preventing STEP 1/2/3 from being incorrectly centered.
|
|
838
|
+
|
|
603
839
|
const isSafeInline = (el) => {
|
|
604
840
|
// 1. Reject Web Components / Custom Elements
|
|
605
841
|
if (el.tagName.includes('-')) return false;
|