@json-to-office/core-pptx 0.19.0 → 0.21.0
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/dist/components/chart.d.ts.map +1 -1
- package/dist/components/highcharts.d.ts +1 -1
- package/dist/components/highcharts.d.ts.map +1 -1
- package/dist/core/generator.d.ts +17 -3
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/packagePresentation.d.ts +17 -0
- package/dist/core/packagePresentation.d.ts.map +1 -0
- package/dist/core/structure.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +391 -92
- package/dist/index.js.map +1 -1
- package/dist/plugin/createPresentationGenerator.d.ts +5 -2
- package/dist/plugin/createPresentationGenerator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +15 -3
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/validation.d.ts +9 -6
- package/dist/plugin/validation.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/color.d.ts +6 -0
- package/dist/utils/color.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/core/generator.ts
|
|
2
|
-
import JSZip from "jszip";
|
|
3
2
|
import { writeFileSync } from "fs";
|
|
4
3
|
|
|
5
4
|
// src/types.ts
|
|
@@ -311,8 +310,7 @@ function resolveComponentTree(components, theme) {
|
|
|
311
310
|
import { mergeWithDefaults as mergeWithDefaults2 } from "@json-to-office/shared";
|
|
312
311
|
function processPresentation(document, options) {
|
|
313
312
|
const { props, children = [] } = document;
|
|
314
|
-
const
|
|
315
|
-
const baseTheme = options?.customThemes?.[themeName] ?? getPptxTheme(themeName);
|
|
313
|
+
const baseTheme = typeof props.theme === "object" && props.theme !== null ? props.theme : options?.customThemes?.[props.theme ?? "default"] ?? getPptxTheme(props.theme ?? "default");
|
|
316
314
|
const presDefaults = props.componentDefaults;
|
|
317
315
|
const theme = presDefaults ? {
|
|
318
316
|
...baseTheme,
|
|
@@ -413,12 +411,25 @@ var SEMANTIC_TO_THEME_KEY = {
|
|
|
413
411
|
bg1: "background",
|
|
414
412
|
bg2: "background2"
|
|
415
413
|
};
|
|
414
|
+
var DEFAULT_CHART_THEME_COLORS = [
|
|
415
|
+
"primary",
|
|
416
|
+
"secondary",
|
|
417
|
+
"accent",
|
|
418
|
+
"accent4",
|
|
419
|
+
"accent5",
|
|
420
|
+
"accent6"
|
|
421
|
+
];
|
|
416
422
|
function resolveColor(color, theme, warnings) {
|
|
417
423
|
const themeKey = SEMANTIC_TO_THEME_KEY[color];
|
|
418
424
|
if (themeKey) {
|
|
419
425
|
const resolved = theme.colors[themeKey];
|
|
420
|
-
if (resolved)
|
|
421
|
-
|
|
426
|
+
if (resolved)
|
|
427
|
+
return resolved.startsWith("#") ? resolved.slice(1) : resolved;
|
|
428
|
+
warn(
|
|
429
|
+
warnings,
|
|
430
|
+
W.THEME_COLOR_FALLBACK,
|
|
431
|
+
`Theme color "${themeKey}" not defined, falling back to primary`
|
|
432
|
+
);
|
|
422
433
|
return theme.colors.primary.startsWith("#") ? theme.colors.primary.slice(1) : theme.colors.primary;
|
|
423
434
|
}
|
|
424
435
|
const bare = color.startsWith("#") ? color.slice(1) : color;
|
|
@@ -426,7 +437,11 @@ function resolveColor(color, theme, warnings) {
|
|
|
426
437
|
return bare[0] + bare[0] + bare[1] + bare[1] + bare[2] + bare[2];
|
|
427
438
|
}
|
|
428
439
|
if (!/^[0-9A-Fa-f]{6}$/.test(bare)) {
|
|
429
|
-
warn(
|
|
440
|
+
warn(
|
|
441
|
+
warnings,
|
|
442
|
+
W.UNKNOWN_COLOR,
|
|
443
|
+
`Unknown color value: "${color}", treating as literal`
|
|
444
|
+
);
|
|
430
445
|
}
|
|
431
446
|
return bare;
|
|
432
447
|
}
|
|
@@ -1113,8 +1128,21 @@ Cause: ${error instanceof Error ? error.message : String(error)}`
|
|
|
1113
1128
|
height: config.options.chart?.height ?? 720
|
|
1114
1129
|
};
|
|
1115
1130
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1131
|
+
function withThemeColors(props, theme, warnings) {
|
|
1132
|
+
if (!props.options || props.options.colors || !theme?.colors) return props;
|
|
1133
|
+
const palette = DEFAULT_CHART_THEME_COLORS.map(
|
|
1134
|
+
(token) => `#${resolveColor(token, theme, warnings)}`
|
|
1135
|
+
);
|
|
1136
|
+
return {
|
|
1137
|
+
...props,
|
|
1138
|
+
options: { ...props.options, colors: palette }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async function renderHighchartsComponent(slide, props, theme, warnings, servicesConfig) {
|
|
1142
|
+
const chart = await generateChart(
|
|
1143
|
+
withThemeColors(props, theme, warnings),
|
|
1144
|
+
servicesConfig
|
|
1145
|
+
);
|
|
1118
1146
|
const w = props.w ?? chart.width / PX_PER_INCH;
|
|
1119
1147
|
const h = props.h ?? chart.height / PX_PER_INCH;
|
|
1120
1148
|
slide.addImage({
|
|
@@ -1138,25 +1166,39 @@ var CHART_TYPE_MAP = {
|
|
|
1138
1166
|
radar: "radar",
|
|
1139
1167
|
scatter: "scatter"
|
|
1140
1168
|
};
|
|
1141
|
-
var DEFAULT_THEME_COLORS =
|
|
1169
|
+
var DEFAULT_THEME_COLORS = DEFAULT_CHART_THEME_COLORS;
|
|
1142
1170
|
function renderChartComponent(slide, props, theme, _pptx, warnings) {
|
|
1143
1171
|
const chartType = CHART_TYPE_MAP[props.type];
|
|
1144
1172
|
if (!chartType) {
|
|
1145
|
-
warn(warnings, W.UNKNOWN_CHART_TYPE, `Unknown chart type: ${props.type}`, {
|
|
1173
|
+
warn(warnings, W.UNKNOWN_CHART_TYPE, `Unknown chart type: ${props.type}`, {
|
|
1174
|
+
component: "chart"
|
|
1175
|
+
});
|
|
1146
1176
|
return;
|
|
1147
1177
|
}
|
|
1148
1178
|
if (!props.data || props.data.length === 0) {
|
|
1149
|
-
warn(warnings, W.CHART_NO_DATA, "Chart component has no data series", {
|
|
1179
|
+
warn(warnings, W.CHART_NO_DATA, "Chart component has no data series", {
|
|
1180
|
+
component: "chart"
|
|
1181
|
+
});
|
|
1150
1182
|
return;
|
|
1151
1183
|
}
|
|
1152
1184
|
for (const series of props.data) {
|
|
1153
1185
|
if (!series.labels || !series.values) {
|
|
1154
|
-
warn(
|
|
1186
|
+
warn(
|
|
1187
|
+
warnings,
|
|
1188
|
+
W.CHART_INVALID_SERIES,
|
|
1189
|
+
`Chart series "${series.name ?? "(unnamed)"}" missing labels or values`,
|
|
1190
|
+
{ component: "chart" }
|
|
1191
|
+
);
|
|
1155
1192
|
return;
|
|
1156
1193
|
}
|
|
1157
1194
|
}
|
|
1158
1195
|
if ((chartType === "pie" || chartType === "doughnut") && props.data.length > 1) {
|
|
1159
|
-
warn(
|
|
1196
|
+
warn(
|
|
1197
|
+
warnings,
|
|
1198
|
+
W.CHART_MULTI_SERIES,
|
|
1199
|
+
`${props.type} chart has ${props.data.length} series \u2014 only the first will render`,
|
|
1200
|
+
{ component: "chart" }
|
|
1201
|
+
);
|
|
1160
1202
|
}
|
|
1161
1203
|
const data = props.data.map((series) => {
|
|
1162
1204
|
const d = {};
|
|
@@ -1185,41 +1227,60 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
|
|
|
1185
1227
|
if (props.showLabel !== void 0) opts.showLabel = props.showLabel;
|
|
1186
1228
|
if (props.showSerName !== void 0) opts.showSerName = props.showSerName;
|
|
1187
1229
|
if (props.title !== void 0) opts.title = props.title;
|
|
1188
|
-
if (props.titleFontSize !== void 0)
|
|
1189
|
-
|
|
1230
|
+
if (props.titleFontSize !== void 0)
|
|
1231
|
+
opts.titleFontSize = props.titleFontSize;
|
|
1232
|
+
if (props.titleFontFace !== void 0)
|
|
1233
|
+
opts.titleFontFace = props.titleFontFace;
|
|
1190
1234
|
if (props.legendPos !== void 0) opts.legendPos = props.legendPos;
|
|
1191
|
-
if (props.legendFontSize !== void 0)
|
|
1192
|
-
|
|
1235
|
+
if (props.legendFontSize !== void 0)
|
|
1236
|
+
opts.legendFontSize = props.legendFontSize;
|
|
1237
|
+
if (props.legendFontFace !== void 0)
|
|
1238
|
+
opts.legendFontFace = props.legendFontFace;
|
|
1193
1239
|
if (props.catAxisTitle !== void 0) {
|
|
1194
1240
|
opts.catAxisTitle = props.catAxisTitle;
|
|
1195
1241
|
opts.showCatAxisTitle = true;
|
|
1196
1242
|
}
|
|
1197
|
-
if (props.catAxisHidden !== void 0)
|
|
1198
|
-
|
|
1199
|
-
if (props.
|
|
1243
|
+
if (props.catAxisHidden !== void 0)
|
|
1244
|
+
opts.catAxisHidden = props.catAxisHidden;
|
|
1245
|
+
if (props.catAxisLabelRotate !== void 0)
|
|
1246
|
+
opts.catAxisLabelRotate = props.catAxisLabelRotate;
|
|
1247
|
+
if (props.catAxisLabelFontSize !== void 0)
|
|
1248
|
+
opts.catAxisLabelFontSize = props.catAxisLabelFontSize;
|
|
1200
1249
|
if (props.valAxisTitle !== void 0) {
|
|
1201
1250
|
opts.valAxisTitle = props.valAxisTitle;
|
|
1202
1251
|
opts.showValAxisTitle = true;
|
|
1203
1252
|
}
|
|
1204
|
-
if (props.valAxisHidden !== void 0)
|
|
1205
|
-
|
|
1206
|
-
if (props.
|
|
1207
|
-
|
|
1208
|
-
if (props.
|
|
1253
|
+
if (props.valAxisHidden !== void 0)
|
|
1254
|
+
opts.valAxisHidden = props.valAxisHidden;
|
|
1255
|
+
if (props.valAxisMinVal !== void 0)
|
|
1256
|
+
opts.valAxisMinVal = props.valAxisMinVal;
|
|
1257
|
+
if (props.valAxisMaxVal !== void 0)
|
|
1258
|
+
opts.valAxisMaxVal = props.valAxisMaxVal;
|
|
1259
|
+
if (props.valAxisLabelFormatCode !== void 0)
|
|
1260
|
+
opts.valAxisLabelFormatCode = props.valAxisLabelFormatCode;
|
|
1261
|
+
if (props.valAxisMajorUnit !== void 0)
|
|
1262
|
+
opts.valAxisMajorUnit = props.valAxisMajorUnit;
|
|
1209
1263
|
if (props.barDir !== void 0) opts.barDir = props.barDir;
|
|
1210
1264
|
if (props.barGrouping !== void 0) opts.barGrouping = props.barGrouping;
|
|
1211
|
-
if (props.barGapWidthPct !== void 0)
|
|
1265
|
+
if (props.barGapWidthPct !== void 0)
|
|
1266
|
+
opts.barGapWidthPct = props.barGapWidthPct;
|
|
1212
1267
|
if (props.lineSmooth !== void 0) opts.lineSmooth = props.lineSmooth;
|
|
1213
|
-
if (props.lineDataSymbol !== void 0)
|
|
1268
|
+
if (props.lineDataSymbol !== void 0)
|
|
1269
|
+
opts.lineDataSymbol = props.lineDataSymbol;
|
|
1214
1270
|
if (props.lineSize !== void 0) opts.lineSize = props.lineSize;
|
|
1215
|
-
if (props.firstSliceAng !== void 0)
|
|
1271
|
+
if (props.firstSliceAng !== void 0)
|
|
1272
|
+
opts.firstSliceAng = props.firstSliceAng;
|
|
1216
1273
|
if (props.holeSize !== void 0) opts.holeSize = props.holeSize;
|
|
1217
1274
|
if (props.radarStyle !== void 0) opts.radarStyle = props.radarStyle;
|
|
1218
1275
|
opts.dataLabelColor = props.dataLabelColor ? resolveColor(props.dataLabelColor, theme, warnings) : themeTextColor;
|
|
1219
|
-
if (props.dataLabelFontSize !== void 0)
|
|
1220
|
-
|
|
1221
|
-
if (props.
|
|
1222
|
-
|
|
1276
|
+
if (props.dataLabelFontSize !== void 0)
|
|
1277
|
+
opts.dataLabelFontSize = props.dataLabelFontSize;
|
|
1278
|
+
if (props.dataLabelFontFace !== void 0)
|
|
1279
|
+
opts.dataLabelFontFace = props.dataLabelFontFace;
|
|
1280
|
+
if (props.dataLabelFontBold !== void 0)
|
|
1281
|
+
opts.dataLabelFontBold = props.dataLabelFontBold;
|
|
1282
|
+
if (props.dataLabelPosition !== void 0)
|
|
1283
|
+
opts.dataLabelPosition = props.dataLabelPosition;
|
|
1223
1284
|
slide.addChart(chartType, data, opts);
|
|
1224
1285
|
}
|
|
1225
1286
|
|
|
@@ -1550,13 +1611,161 @@ async function resolveDocumentFonts(document, theme, warnings, fonts) {
|
|
|
1550
1611
|
|
|
1551
1612
|
// src/core/generator.ts
|
|
1552
1613
|
import { applyExportMode, scopedThemeName } from "@json-to-office/shared";
|
|
1553
|
-
import {
|
|
1614
|
+
import {
|
|
1615
|
+
collectImageSourceConflicts,
|
|
1616
|
+
validateJsonPresentationDocument,
|
|
1617
|
+
validatePresentationDocument
|
|
1618
|
+
} from "@json-to-office/shared-pptx";
|
|
1619
|
+
|
|
1620
|
+
// src/core/packagePresentation.ts
|
|
1621
|
+
import JSZip from "jszip";
|
|
1622
|
+
var MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
|
|
1623
|
+
var NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
|
|
1624
|
+
var DEFAULT_GENERATED_AT = "2000-01-01T00:00:00.000Z";
|
|
1625
|
+
function resolveGeneratedAt(value) {
|
|
1626
|
+
const date = value === void 0 ? new Date(DEFAULT_GENERATED_AT) : new Date(value);
|
|
1627
|
+
if (Number.isNaN(date.getTime())) {
|
|
1628
|
+
throw new Error(`Invalid generatedAt value: ${String(value)}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (date.getUTCFullYear() < 1980) {
|
|
1631
|
+
throw new Error(
|
|
1632
|
+
"generatedAt must be on or after 1980-01-01 for ZIP compatibility"
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return date;
|
|
1636
|
+
}
|
|
1637
|
+
function replaceCoreTimestamp(xml, tag, value) {
|
|
1638
|
+
const expression = new RegExp(
|
|
1639
|
+
`(<dcterms:${tag}\\b[^>]*>)[^<]*(</dcterms:${tag}>)`,
|
|
1640
|
+
"g"
|
|
1641
|
+
);
|
|
1642
|
+
return xml.replace(expression, `$1${value}$2`);
|
|
1643
|
+
}
|
|
1644
|
+
var EMBEDDED_OFFICE_PACKAGE = /\.(?:docx|pptx|xlsx|xlsm)$/i;
|
|
1645
|
+
function remapChartReferences(value, chartIds) {
|
|
1646
|
+
return value.replace(/chart(\d+)\.xml/g, (match, rawId) => {
|
|
1647
|
+
const id = chartIds.get(Number(rawId));
|
|
1648
|
+
return id === void 0 ? match : `chart${id}.xml`;
|
|
1649
|
+
}).replace(
|
|
1650
|
+
/Microsoft_Excel_Worksheet(\d+)\.xlsx/g,
|
|
1651
|
+
(match, rawId) => {
|
|
1652
|
+
const id = chartIds.get(Number(rawId));
|
|
1653
|
+
return id === void 0 ? match : `Microsoft_Excel_Worksheet${id}.xlsx`;
|
|
1654
|
+
}
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
async function canonicalizeChartIds(zip) {
|
|
1658
|
+
const sourceIds = Object.keys(zip.files).map((path2) => path2.match(/^ppt\/charts\/chart(\d+)\.xml$/)?.[1]).filter((value) => value !== void 0).map(Number).sort((a, b) => a - b);
|
|
1659
|
+
const chartIds = new Map(sourceIds.map((id, index) => [id, index + 1]));
|
|
1660
|
+
if (chartIds.size === 0) return;
|
|
1661
|
+
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1662
|
+
if (entry.dir || !path2.endsWith(".xml") && !path2.endsWith(".rels")) {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const xml = await entry.async("string");
|
|
1666
|
+
const remapped = remapChartReferences(xml, chartIds);
|
|
1667
|
+
if (remapped !== xml) zip.file(path2, remapped);
|
|
1668
|
+
}
|
|
1669
|
+
const renames = [];
|
|
1670
|
+
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1671
|
+
if (entry.dir) continue;
|
|
1672
|
+
const remappedPath = remapChartReferences(path2, chartIds);
|
|
1673
|
+
if (remappedPath === path2) continue;
|
|
1674
|
+
renames.push({
|
|
1675
|
+
from: path2,
|
|
1676
|
+
to: remappedPath,
|
|
1677
|
+
data: await entry.async("nodebuffer"),
|
|
1678
|
+
date: entry.date
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
for (const entry of renames) zip.remove(entry.from);
|
|
1682
|
+
for (const entry of renames) {
|
|
1683
|
+
zip.file(entry.to, entry.data, { date: entry.date });
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
async function generateZip(zip) {
|
|
1687
|
+
return await zip.generateAsync({
|
|
1688
|
+
type: "nodebuffer",
|
|
1689
|
+
compression: "DEFLATE",
|
|
1690
|
+
compressionOptions: { level: 6 },
|
|
1691
|
+
platform: "DOS",
|
|
1692
|
+
streamFiles: false
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
async function canonicalizePackage(zip, generatedAt, depth = 0) {
|
|
1696
|
+
const timestamp = generatedAt.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1697
|
+
const coreEntry = zip.file("docProps/core.xml");
|
|
1698
|
+
if (coreEntry) {
|
|
1699
|
+
let coreXml = await coreEntry.async("string");
|
|
1700
|
+
coreXml = replaceCoreTimestamp(coreXml, "created", timestamp);
|
|
1701
|
+
coreXml = replaceCoreTimestamp(coreXml, "modified", timestamp);
|
|
1702
|
+
zip.file("docProps/core.xml", coreXml);
|
|
1703
|
+
}
|
|
1704
|
+
if (depth < 3) {
|
|
1705
|
+
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1706
|
+
if (entry.dir || !EMBEDDED_OFFICE_PACKAGE.test(path2)) continue;
|
|
1707
|
+
try {
|
|
1708
|
+
const nested = await JSZip.loadAsync(await entry.async("nodebuffer"));
|
|
1709
|
+
await canonicalizePackage(nested, generatedAt, depth + 1);
|
|
1710
|
+
zip.file(path2, await generateZip(nested));
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const entry of Object.values(zip.files)) {
|
|
1716
|
+
entry.date = generatedAt;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
async function packagePresentationBuffer(buffer, options = {}) {
|
|
1720
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
1721
|
+
let changed = false;
|
|
1722
|
+
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1723
|
+
if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
|
|
1724
|
+
const xml = await entry.async("string");
|
|
1725
|
+
if (xml.includes(MEDIUM_STYLE_2_ACCENT_1)) {
|
|
1726
|
+
zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID));
|
|
1727
|
+
changed = true;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (options.deterministic !== false) {
|
|
1731
|
+
const generatedAt = resolveGeneratedAt(options.generatedAt);
|
|
1732
|
+
await canonicalizeChartIds(zip);
|
|
1733
|
+
await canonicalizePackage(zip, generatedAt);
|
|
1734
|
+
changed = true;
|
|
1735
|
+
}
|
|
1736
|
+
if (!changed) return buffer;
|
|
1737
|
+
return generateZip(zip);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/core/generator.ts
|
|
1741
|
+
var PresentationValidationError = class extends Error {
|
|
1742
|
+
errors;
|
|
1743
|
+
constructor(errors) {
|
|
1744
|
+
super(
|
|
1745
|
+
`Presentation validation failed:
|
|
1746
|
+
${errors.map((error) => ` - ${error.path}: ${error.message}`).join("\n")}`
|
|
1747
|
+
);
|
|
1748
|
+
this.name = "PresentationValidationError";
|
|
1749
|
+
this.errors = errors;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
function assertValidPresentation(input, validation) {
|
|
1753
|
+
if (validation?.enabled === false) return;
|
|
1754
|
+
const options = {
|
|
1755
|
+
allowUnknownFields: validation?.allowUnknownFields
|
|
1756
|
+
};
|
|
1757
|
+
const result = typeof input === "string" ? validateJsonPresentationDocument(input, options) : validatePresentationDocument(input, options);
|
|
1758
|
+
if (!result.valid) {
|
|
1759
|
+
throw new PresentationValidationError(result.errors);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1554
1762
|
function isPresentationComponentDefinition(definition) {
|
|
1555
1763
|
if (typeof definition !== "object" || definition === null) return false;
|
|
1556
1764
|
const def = definition;
|
|
1557
1765
|
return def.name === "pptx" && "props" in def;
|
|
1558
1766
|
}
|
|
1559
1767
|
async function generatePresentation(document, options, warnings) {
|
|
1768
|
+
assertValidPresentation(document, options?.validation);
|
|
1560
1769
|
if (!document || document.name !== "pptx") {
|
|
1561
1770
|
throw new Error("Top-level component must be a pptx component");
|
|
1562
1771
|
}
|
|
@@ -1575,6 +1784,7 @@ async function generateBufferFromJson(jsonConfig, options) {
|
|
|
1575
1784
|
return result.buffer;
|
|
1576
1785
|
}
|
|
1577
1786
|
async function generateBufferWithWarnings(jsonConfig, options) {
|
|
1787
|
+
assertValidPresentation(jsonConfig, options?.validation);
|
|
1578
1788
|
let component;
|
|
1579
1789
|
if (typeof jsonConfig === "string") {
|
|
1580
1790
|
const parsed = JSON.parse(jsonConfig);
|
|
@@ -1586,6 +1796,21 @@ async function generateBufferWithWarnings(jsonConfig, options) {
|
|
|
1586
1796
|
component = jsonConfig;
|
|
1587
1797
|
}
|
|
1588
1798
|
const warnings = [];
|
|
1799
|
+
if (typeof component.props?.theme === "object" && component.props.theme !== null) {
|
|
1800
|
+
const inlineTheme = component.props.theme;
|
|
1801
|
+
const inlineName = inlineTheme.name || "inline-theme";
|
|
1802
|
+
component = {
|
|
1803
|
+
...component,
|
|
1804
|
+
props: { ...component.props, theme: inlineName }
|
|
1805
|
+
};
|
|
1806
|
+
options = {
|
|
1807
|
+
...options,
|
|
1808
|
+
customThemes: {
|
|
1809
|
+
...options?.customThemes ?? {},
|
|
1810
|
+
[inlineName]: inlineTheme
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1589
1814
|
const baseThemeName = component.props?.theme ?? "default";
|
|
1590
1815
|
let resolvedTheme = options?.customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName);
|
|
1591
1816
|
const mode = applyExportMode({
|
|
@@ -1628,36 +1853,21 @@ async function generateBufferWithWarnings(jsonConfig, options) {
|
|
|
1628
1853
|
warnings
|
|
1629
1854
|
);
|
|
1630
1855
|
const data = await pptx.write({ outputType: "nodebuffer" });
|
|
1631
|
-
const buffer = await
|
|
1856
|
+
const buffer = await packagePresentationBuffer(data, options);
|
|
1632
1857
|
return { buffer, warnings };
|
|
1633
1858
|
}
|
|
1634
1859
|
async function generateAndSaveFromJson(jsonConfig, outputPath, options) {
|
|
1635
1860
|
const buffer = await generateBufferFromJson(jsonConfig, options);
|
|
1636
1861
|
writeFileSync(outputPath, buffer);
|
|
1637
1862
|
}
|
|
1638
|
-
async function generateFromFile(filePath, outputPath) {
|
|
1863
|
+
async function generateFromFile(filePath, outputPath, options) {
|
|
1639
1864
|
const { readFileSync } = await import("fs");
|
|
1640
1865
|
const json = readFileSync(filePath, "utf-8");
|
|
1641
|
-
await generateAndSaveFromJson(json, outputPath);
|
|
1866
|
+
await generateAndSaveFromJson(json, outputPath, options);
|
|
1642
1867
|
}
|
|
1643
|
-
|
|
1644
|
-
var NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
|
|
1645
|
-
async function neutralizeTableStyle(buffer) {
|
|
1646
|
-
const zip = await JSZip.loadAsync(buffer);
|
|
1647
|
-
let changed = false;
|
|
1648
|
-
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1649
|
-
if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
|
|
1650
|
-
const xml = await entry.async("string");
|
|
1651
|
-
if (xml.includes(MEDIUM_STYLE_2_ACCENT_1)) {
|
|
1652
|
-
zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID));
|
|
1653
|
-
changed = true;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
return changed ? await zip.generateAsync({ type: "nodebuffer" }) : buffer;
|
|
1657
|
-
}
|
|
1658
|
-
async function savePresentation(pptx, outputPath) {
|
|
1868
|
+
async function savePresentation(pptx, outputPath, options) {
|
|
1659
1869
|
const data = await pptx.write({ outputType: "nodebuffer" });
|
|
1660
|
-
const buffer = await
|
|
1870
|
+
const buffer = await packagePresentationBuffer(data, options);
|
|
1661
1871
|
writeFileSync(outputPath, buffer);
|
|
1662
1872
|
}
|
|
1663
1873
|
var PresentationGenerator = {
|
|
@@ -1677,7 +1887,6 @@ import {
|
|
|
1677
1887
|
} from "@json-to-office/shared/plugin";
|
|
1678
1888
|
|
|
1679
1889
|
// src/plugin/createPresentationGenerator.ts
|
|
1680
|
-
import JSZip2 from "jszip";
|
|
1681
1890
|
import {
|
|
1682
1891
|
resolveComponentVersion as resolveComponentVersion2,
|
|
1683
1892
|
DuplicateComponentError as DuplicateComponentError2,
|
|
@@ -1690,23 +1899,33 @@ import {
|
|
|
1690
1899
|
validateCustomComponentProps,
|
|
1691
1900
|
ComponentValidationError
|
|
1692
1901
|
} from "@json-to-office/shared/plugin";
|
|
1693
|
-
import {
|
|
1902
|
+
import { validatePresentationDocument as validatePresentationDocument2 } from "@json-to-office/shared-pptx";
|
|
1694
1903
|
import {
|
|
1695
1904
|
DuplicateComponentError,
|
|
1696
1905
|
ComponentValidationError as ComponentValidationError2
|
|
1697
1906
|
} from "@json-to-office/shared/plugin";
|
|
1698
|
-
function validateComponentProps(schema, props, componentName) {
|
|
1907
|
+
function validateComponentProps(schema, props, componentName, opts) {
|
|
1699
1908
|
return validateCustomComponentProps(schema.propsSchema, props, {
|
|
1700
|
-
|
|
1701
|
-
|
|
1909
|
+
// Render-time cleaning remains the default. The document-validation path
|
|
1910
|
+
// passes clean:false so unknown custom props are rejected when the custom
|
|
1911
|
+
// schema declares additionalProperties:false.
|
|
1912
|
+
clean: opts?.clean ?? true,
|
|
1913
|
+
applyDefaults: opts?.applyDefaults ?? true,
|
|
1702
1914
|
componentName
|
|
1703
1915
|
});
|
|
1704
1916
|
}
|
|
1705
|
-
function validatePresentation(document, customComponents) {
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1917
|
+
function validatePresentation(document, customComponents, options) {
|
|
1918
|
+
const knownCustomNames = new Set(customComponents.map((c) => c.name));
|
|
1919
|
+
const documentResult = validatePresentationDocument2(document, {
|
|
1920
|
+
knownCustomNames,
|
|
1921
|
+
allowUnknownFields: options?.allowUnknownFields
|
|
1922
|
+
});
|
|
1923
|
+
const errors = [...documentResult.errors];
|
|
1708
1924
|
function validateComponents(components, pathPrefix = "children") {
|
|
1709
1925
|
components.forEach((componentData, index) => {
|
|
1926
|
+
if (!componentData || typeof componentData !== "object" || Array.isArray(componentData)) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1710
1929
|
const customComponent = customComponents.find(
|
|
1711
1930
|
(cc) => cc.name === componentData.name
|
|
1712
1931
|
);
|
|
@@ -1719,7 +1938,8 @@ function validatePresentation(document, customComponents) {
|
|
|
1719
1938
|
const validation = validateComponentProps(
|
|
1720
1939
|
versionEntry,
|
|
1721
1940
|
componentData.props,
|
|
1722
|
-
customComponent.name
|
|
1941
|
+
customComponent.name,
|
|
1942
|
+
{ clean: options?.allowUnknownFields === true }
|
|
1723
1943
|
);
|
|
1724
1944
|
if (!validation.valid && validation.errors) {
|
|
1725
1945
|
const indexedErrors = validation.errors.map(
|
|
@@ -1739,7 +1959,7 @@ function validatePresentation(document, customComponents) {
|
|
|
1739
1959
|
}
|
|
1740
1960
|
});
|
|
1741
1961
|
}
|
|
1742
|
-
if (document.children) {
|
|
1962
|
+
if (document && Array.isArray(document.children)) {
|
|
1743
1963
|
validateComponents(document.children);
|
|
1744
1964
|
}
|
|
1745
1965
|
return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [] };
|
|
@@ -1789,24 +2009,9 @@ async function exportPluginSchema(customComponents, outputPath, options = {}) {
|
|
|
1789
2009
|
|
|
1790
2010
|
// src/plugin/createPresentationGenerator.ts
|
|
1791
2011
|
import { applyExportMode as applyExportMode2, scopedThemeName as scopedThemeName2 } from "@json-to-office/shared";
|
|
1792
|
-
var MEDIUM_STYLE_2_ACCENT_12 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
|
|
1793
|
-
var NO_STYLE_NO_GRID2 = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
|
|
1794
|
-
async function neutralizeTableStyle2(buffer) {
|
|
1795
|
-
const zip = await JSZip2.loadAsync(buffer);
|
|
1796
|
-
let changed = false;
|
|
1797
|
-
for (const [path2, entry] of Object.entries(zip.files)) {
|
|
1798
|
-
if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
|
|
1799
|
-
const xml = await entry.async("string");
|
|
1800
|
-
if (xml.includes(MEDIUM_STYLE_2_ACCENT_12)) {
|
|
1801
|
-
zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_12, NO_STYLE_NO_GRID2));
|
|
1802
|
-
changed = true;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
return changed ? await zip.generateAsync({ type: "nodebuffer" }) : buffer;
|
|
1806
|
-
}
|
|
1807
2012
|
function createBuilderImpl(state) {
|
|
1808
2013
|
const componentMap = new Map(state.components.map((c) => [c.name, c]));
|
|
1809
|
-
async function processSlideComponents(components, warningsCollector, theme, depth = 0) {
|
|
2014
|
+
async function processSlideComponents(components, warningsCollector, theme, validateEmitted, parentName, depth = 0) {
|
|
1810
2015
|
if (depth > 20) {
|
|
1811
2016
|
throw new Error(
|
|
1812
2017
|
"Maximum component nesting depth exceeded (20). Check for circular component references."
|
|
@@ -1838,6 +2043,8 @@ function createBuilderImpl(state) {
|
|
|
1838
2043
|
componentWithVersion.children,
|
|
1839
2044
|
warningsCollector,
|
|
1840
2045
|
theme,
|
|
2046
|
+
validateEmitted,
|
|
2047
|
+
void 0,
|
|
1841
2048
|
depth + 1
|
|
1842
2049
|
);
|
|
1843
2050
|
}
|
|
@@ -1857,10 +2064,13 @@ function createBuilderImpl(state) {
|
|
|
1857
2064
|
children: nestedChildren
|
|
1858
2065
|
});
|
|
1859
2066
|
const resultComponents = Array.isArray(result) ? result : [result];
|
|
2067
|
+
validateEmitted?.(resultComponents, versionLabel, parentName);
|
|
1860
2068
|
const processedResult = await processSlideComponents(
|
|
1861
2069
|
resultComponents,
|
|
1862
2070
|
warningsCollector,
|
|
1863
2071
|
theme,
|
|
2072
|
+
validateEmitted,
|
|
2073
|
+
parentName,
|
|
1864
2074
|
depth + 1
|
|
1865
2075
|
);
|
|
1866
2076
|
processed.push(...processedResult);
|
|
@@ -1884,6 +2094,8 @@ function createBuilderImpl(state) {
|
|
|
1884
2094
|
componentData.children,
|
|
1885
2095
|
warningsCollector,
|
|
1886
2096
|
theme,
|
|
2097
|
+
validateEmitted,
|
|
2098
|
+
componentData.name,
|
|
1887
2099
|
depth + 1
|
|
1888
2100
|
);
|
|
1889
2101
|
processed.push({
|
|
@@ -1913,21 +2125,47 @@ function createBuilderImpl(state) {
|
|
|
1913
2125
|
customThemes: state.customThemes,
|
|
1914
2126
|
debug: state.debug,
|
|
1915
2127
|
services: state.services,
|
|
1916
|
-
fonts: state.fonts
|
|
2128
|
+
fonts: state.fonts,
|
|
2129
|
+
validation: state.validation,
|
|
2130
|
+
packaging: state.packaging
|
|
1917
2131
|
};
|
|
1918
2132
|
return createBuilderImpl(
|
|
1919
2133
|
newState
|
|
1920
2134
|
);
|
|
1921
2135
|
}
|
|
1922
|
-
async function generate(document) {
|
|
2136
|
+
async function generate(document, options) {
|
|
1923
2137
|
try {
|
|
1924
|
-
|
|
1925
|
-
|
|
2138
|
+
let internalDocument = document;
|
|
2139
|
+
const validationOptions = {
|
|
2140
|
+
...state.validation,
|
|
2141
|
+
...options?.validation
|
|
2142
|
+
};
|
|
2143
|
+
if (validationOptions.enabled !== false) {
|
|
2144
|
+
const result = validatePresentation(
|
|
2145
|
+
internalDocument,
|
|
2146
|
+
state.components,
|
|
2147
|
+
{ allowUnknownFields: validationOptions.allowUnknownFields }
|
|
2148
|
+
);
|
|
2149
|
+
if (!result.valid) {
|
|
2150
|
+
throw new ComponentValidationError3(result.errors, internalDocument);
|
|
2151
|
+
}
|
|
2152
|
+
} else if (!internalDocument || internalDocument.name !== "pptx") {
|
|
1926
2153
|
throw new Error("Top-level component must be a pptx component");
|
|
1927
2154
|
}
|
|
2155
|
+
let inlineTheme;
|
|
2156
|
+
if (typeof internalDocument.props.theme === "object" && internalDocument.props.theme !== null) {
|
|
2157
|
+
inlineTheme = internalDocument.props.theme;
|
|
2158
|
+
internalDocument = {
|
|
2159
|
+
...internalDocument,
|
|
2160
|
+
props: {
|
|
2161
|
+
...internalDocument.props,
|
|
2162
|
+
theme: inlineTheme.name || "inline-theme"
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
1928
2166
|
const docThemeName = internalDocument.props.theme;
|
|
1929
2167
|
const baseThemeName = docThemeName ?? (typeof state.theme === "string" ? state.theme : "default");
|
|
1930
|
-
let resolvedTheme = state.customThemes?.[baseThemeName] ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(baseThemeName));
|
|
2168
|
+
let resolvedTheme = inlineTheme ?? state.customThemes?.[baseThemeName] ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(baseThemeName));
|
|
1931
2169
|
const warnings = [];
|
|
1932
2170
|
const mode = applyExportMode2({
|
|
1933
2171
|
doc: internalDocument,
|
|
@@ -1942,7 +2180,39 @@ function createBuilderImpl(state) {
|
|
|
1942
2180
|
component: "fontRegistry"
|
|
1943
2181
|
});
|
|
1944
2182
|
}
|
|
1945
|
-
const
|
|
2183
|
+
const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
|
|
2184
|
+
let validationDocument;
|
|
2185
|
+
if (parentName === "pptx") {
|
|
2186
|
+
validationDocument = { ...mode.doc, children: emitted };
|
|
2187
|
+
} else if (parentName === "slide") {
|
|
2188
|
+
validationDocument = {
|
|
2189
|
+
...mode.doc,
|
|
2190
|
+
children: [{ name: "slide", props: {}, children: emitted }]
|
|
2191
|
+
};
|
|
2192
|
+
} else {
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const result = validatePresentation(
|
|
2196
|
+
validationDocument,
|
|
2197
|
+
state.components,
|
|
2198
|
+
{ allowUnknownFields: validationOptions.allowUnknownFields }
|
|
2199
|
+
);
|
|
2200
|
+
if (!result.valid) {
|
|
2201
|
+
throw new ComponentValidationError3(
|
|
2202
|
+
result.errors.map((error) => ({
|
|
2203
|
+
...error,
|
|
2204
|
+
message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
|
|
2205
|
+
})),
|
|
2206
|
+
emitted
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
const processedChildren = mode.doc.children ? await processAllSlides(
|
|
2211
|
+
mode.doc.children,
|
|
2212
|
+
warnings,
|
|
2213
|
+
resolvedTheme,
|
|
2214
|
+
validateEmitted
|
|
2215
|
+
) : [];
|
|
1946
2216
|
const themeName = scopedThemeName2(baseThemeName, state.fonts?.mode);
|
|
1947
2217
|
const docWithScopedTheme = themeName !== baseThemeName ? {
|
|
1948
2218
|
...mode.doc,
|
|
@@ -1950,6 +2220,20 @@ function createBuilderImpl(state) {
|
|
|
1950
2220
|
children: processedChildren
|
|
1951
2221
|
} : { ...mode.doc, children: processedChildren };
|
|
1952
2222
|
const processedDocument = docWithScopedTheme;
|
|
2223
|
+
if (validationOptions.enabled !== false) {
|
|
2224
|
+
const result = validatePresentation(processedDocument, [], {
|
|
2225
|
+
allowUnknownFields: validationOptions.allowUnknownFields
|
|
2226
|
+
});
|
|
2227
|
+
if (!result.valid) {
|
|
2228
|
+
throw new ComponentValidationError3(
|
|
2229
|
+
result.errors.map((error) => ({
|
|
2230
|
+
...error,
|
|
2231
|
+
message: `expanded plugin output failed validation \u2014 ${error.message}`
|
|
2232
|
+
})),
|
|
2233
|
+
processedDocument
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
1953
2237
|
await resolveDocumentFonts(
|
|
1954
2238
|
processedDocument,
|
|
1955
2239
|
resolvedTheme,
|
|
@@ -1966,7 +2250,10 @@ function createBuilderImpl(state) {
|
|
|
1966
2250
|
});
|
|
1967
2251
|
const pptx = await renderPresentation(processed, warnings);
|
|
1968
2252
|
const data = await pptx.write({ outputType: "nodebuffer" });
|
|
1969
|
-
const buffer = await
|
|
2253
|
+
const buffer = await packagePresentationBuffer(data, {
|
|
2254
|
+
deterministic: options?.deterministic ?? state.packaging.deterministic,
|
|
2255
|
+
generatedAt: options?.generatedAt ?? state.packaging.generatedAt
|
|
2256
|
+
});
|
|
1970
2257
|
return { buffer, warnings };
|
|
1971
2258
|
} catch (error) {
|
|
1972
2259
|
if (state.debug) {
|
|
@@ -1975,29 +2262,33 @@ function createBuilderImpl(state) {
|
|
|
1975
2262
|
throw error;
|
|
1976
2263
|
}
|
|
1977
2264
|
}
|
|
1978
|
-
async function processAllSlides(children, warnings, theme) {
|
|
2265
|
+
async function processAllSlides(children, warnings, theme, validateEmitted) {
|
|
1979
2266
|
const result = [];
|
|
1980
2267
|
for (const child of children) {
|
|
1981
2268
|
if (child.name === "slide" && child.children) {
|
|
1982
2269
|
const processedSlideChildren = await processSlideComponents(
|
|
1983
2270
|
child.children,
|
|
1984
2271
|
warnings,
|
|
1985
|
-
theme
|
|
2272
|
+
theme,
|
|
2273
|
+
validateEmitted,
|
|
2274
|
+
"slide"
|
|
1986
2275
|
);
|
|
1987
2276
|
result.push({ ...child, children: processedSlideChildren });
|
|
1988
2277
|
} else {
|
|
1989
2278
|
const processedTopLevel = await processSlideComponents(
|
|
1990
2279
|
[child],
|
|
1991
2280
|
warnings,
|
|
1992
|
-
theme
|
|
2281
|
+
theme,
|
|
2282
|
+
validateEmitted,
|
|
2283
|
+
"pptx"
|
|
1993
2284
|
);
|
|
1994
2285
|
result.push(...processedTopLevel);
|
|
1995
2286
|
}
|
|
1996
2287
|
}
|
|
1997
2288
|
return result;
|
|
1998
2289
|
}
|
|
1999
|
-
async function generateFile(document, outputPath) {
|
|
2000
|
-
const { buffer, warnings } = await generate(document);
|
|
2290
|
+
async function generateFile(document, outputPath, options) {
|
|
2291
|
+
const { buffer, warnings } = await generate(document, options);
|
|
2001
2292
|
const fs = await import("fs/promises");
|
|
2002
2293
|
await fs.writeFile(outputPath, new Uint8Array(buffer));
|
|
2003
2294
|
return { warnings };
|
|
@@ -2074,7 +2365,12 @@ function createPresentationGenerator(options = {}) {
|
|
|
2074
2365
|
customThemes: options.customThemes,
|
|
2075
2366
|
debug: options.debug ?? false,
|
|
2076
2367
|
services: options.services,
|
|
2077
|
-
fonts: options.fonts
|
|
2368
|
+
fonts: options.fonts,
|
|
2369
|
+
validation: options.validation,
|
|
2370
|
+
packaging: {
|
|
2371
|
+
deterministic: options.deterministic,
|
|
2372
|
+
generatedAt: options.generatedAt
|
|
2373
|
+
}
|
|
2078
2374
|
};
|
|
2079
2375
|
return createBuilderImpl(initialState);
|
|
2080
2376
|
}
|
|
@@ -2088,9 +2384,11 @@ function getPptxCoreVersion() {
|
|
|
2088
2384
|
}
|
|
2089
2385
|
export {
|
|
2090
2386
|
ComponentValidationError2 as ComponentValidationError,
|
|
2387
|
+
DEFAULT_GENERATED_AT,
|
|
2091
2388
|
DEFAULT_PPTX_THEME,
|
|
2092
2389
|
DuplicateComponentError,
|
|
2093
2390
|
PresentationGenerator,
|
|
2391
|
+
PresentationValidationError,
|
|
2094
2392
|
W as WarningCodes,
|
|
2095
2393
|
cleanComponentProps,
|
|
2096
2394
|
createComponent,
|
|
@@ -2108,6 +2406,7 @@ export {
|
|
|
2108
2406
|
isPresentationComponent,
|
|
2109
2407
|
isPresentationComponentDefinition,
|
|
2110
2408
|
isSlideComponent,
|
|
2409
|
+
packagePresentationBuffer,
|
|
2111
2410
|
pptxThemes,
|
|
2112
2411
|
renderComponent,
|
|
2113
2412
|
renderHighchartsComponent,
|