@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/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 themeName = props.theme ?? "default";
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) return resolved.startsWith("#") ? resolved.slice(1) : resolved;
421
- warn(warnings, W.THEME_COLOR_FALLBACK, `Theme color "${themeKey}" not defined, falling back to primary`);
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(warnings, W.UNKNOWN_COLOR, `Unknown color value: "${color}", treating as literal`);
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
- async function renderHighchartsComponent(slide, props, _theme, _warnings, servicesConfig) {
1117
- const chart = await generateChart(props, servicesConfig);
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 = ["primary", "secondary", "accent", "accent4", "accent5", "accent6"];
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}`, { component: "chart" });
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", { component: "chart" });
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(warnings, W.CHART_INVALID_SERIES, `Chart series "${series.name ?? "(unnamed)"}" missing labels or values`, { component: "chart" });
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(warnings, W.CHART_MULTI_SERIES, `${props.type} chart has ${props.data.length} series \u2014 only the first will render`, { component: "chart" });
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) opts.titleFontSize = props.titleFontSize;
1189
- if (props.titleFontFace !== void 0) opts.titleFontFace = props.titleFontFace;
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) opts.legendFontSize = props.legendFontSize;
1192
- if (props.legendFontFace !== void 0) opts.legendFontFace = props.legendFontFace;
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) opts.catAxisHidden = props.catAxisHidden;
1198
- if (props.catAxisLabelRotate !== void 0) opts.catAxisLabelRotate = props.catAxisLabelRotate;
1199
- if (props.catAxisLabelFontSize !== void 0) opts.catAxisLabelFontSize = props.catAxisLabelFontSize;
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) opts.valAxisHidden = props.valAxisHidden;
1205
- if (props.valAxisMinVal !== void 0) opts.valAxisMinVal = props.valAxisMinVal;
1206
- if (props.valAxisMaxVal !== void 0) opts.valAxisMaxVal = props.valAxisMaxVal;
1207
- if (props.valAxisLabelFormatCode !== void 0) opts.valAxisLabelFormatCode = props.valAxisLabelFormatCode;
1208
- if (props.valAxisMajorUnit !== void 0) opts.valAxisMajorUnit = props.valAxisMajorUnit;
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) opts.barGapWidthPct = props.barGapWidthPct;
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) opts.lineDataSymbol = props.lineDataSymbol;
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) opts.firstSliceAng = props.firstSliceAng;
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) opts.dataLabelFontSize = props.dataLabelFontSize;
1220
- if (props.dataLabelFontFace !== void 0) opts.dataLabelFontFace = props.dataLabelFontFace;
1221
- if (props.dataLabelFontBold !== void 0) opts.dataLabelFontBold = props.dataLabelFontBold;
1222
- if (props.dataLabelPosition !== void 0) opts.dataLabelPosition = props.dataLabelPosition;
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 { collectImageSourceConflicts } from "@json-to-office/shared-pptx";
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 neutralizeTableStyle(data);
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
- var MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
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 neutralizeTableStyle(data);
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 { collectImageSourceConflicts as collectImageSourceConflicts2 } from "@json-to-office/shared-pptx";
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
- clean: true,
1701
- applyDefaults: true,
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 errors = [];
1707
- errors.push(...collectImageSourceConflicts2(document));
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
- const internalDocument = document;
1925
- if (!internalDocument || internalDocument.name !== "pptx") {
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 processedChildren = mode.doc.children ? await processAllSlides(mode.doc.children, warnings, resolvedTheme) : [];
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 neutralizeTableStyle2(data);
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,