@json-to-office/core-pptx 0.20.0 → 0.22.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
@@ -307,8 +306,82 @@ function resolveComponentTree(components, theme) {
307
306
  });
308
307
  }
309
308
 
309
+ // src/utils/hyperlink.ts
310
+ var HYPERLINK_SLIDE_UNRESOLVED = "HYPERLINK_SLIDE_UNRESOLVED";
311
+ function remapHyperlink(hyperlink, map) {
312
+ if (hyperlink.url || hyperlink.slide == null) return hyperlink;
313
+ const rendered = map.get(hyperlink.slide);
314
+ if (rendered === void 0) {
315
+ const { slide, ...rest } = hyperlink;
316
+ return { ...rest, unresolvedSlideRef: slide };
317
+ }
318
+ return rendered === hyperlink.slide ? hyperlink : { ...hyperlink, slide: rendered };
319
+ }
320
+ function remapHyperlinkProps(props, map) {
321
+ const hyperlink = props.hyperlink;
322
+ if (!hyperlink || typeof hyperlink !== "object") return props;
323
+ const remapped = remapHyperlink(hyperlink, map);
324
+ return remapped === hyperlink ? props : { ...props, hyperlink: remapped };
325
+ }
326
+ function remapHyperlinkSlideRefs(component, map) {
327
+ const hyperlink = component.props?.hyperlink;
328
+ let next = component;
329
+ if (hyperlink && typeof hyperlink === "object") {
330
+ const remapped = remapHyperlink(hyperlink, map);
331
+ if (remapped !== hyperlink) {
332
+ next = { ...next, props: { ...next.props, hyperlink: remapped } };
333
+ }
334
+ }
335
+ if (next.children && next.children.length > 0) {
336
+ next = {
337
+ ...next,
338
+ children: next.children.map(
339
+ (child) => remapHyperlinkSlideRefs(child, map)
340
+ )
341
+ };
342
+ }
343
+ return next;
344
+ }
345
+ function applyHyperlink(opts, hyperlink, componentName, warnings) {
346
+ if (!hyperlink) return;
347
+ if (hyperlink.url) {
348
+ opts.hyperlink = { url: hyperlink.url, tooltip: hyperlink.tooltip };
349
+ return;
350
+ }
351
+ if (hyperlink.unresolvedSlideRef != null) {
352
+ const message = `hyperlink.slide ${hyperlink.unresolvedSlideRef} matches no slide in the generated presentation (slide disabled, or index out of range) \u2014 hyperlink dropped`;
353
+ if (warnings) {
354
+ warnings.push({
355
+ code: HYPERLINK_SLIDE_UNRESOLVED,
356
+ message,
357
+ component: componentName
358
+ });
359
+ } else {
360
+ console.warn(message);
361
+ }
362
+ return;
363
+ }
364
+ if (hyperlink.slide) {
365
+ opts.hyperlink = { slide: hyperlink.slide, tooltip: hyperlink.tooltip };
366
+ }
367
+ }
368
+
310
369
  // src/core/structure.ts
311
370
  import { mergeWithDefaults as mergeWithDefaults2 } from "@json-to-office/shared";
371
+ function isSlideEnabled(child) {
372
+ return !("enabled" in child && child.enabled === false);
373
+ }
374
+ function buildSlideIndexMap(children) {
375
+ const map = /* @__PURE__ */ new Map();
376
+ let authored = 0;
377
+ let rendered = 0;
378
+ for (const child of children) {
379
+ if (!isSlideComponent(child)) continue;
380
+ authored++;
381
+ if (isSlideEnabled(child)) map.set(authored, ++rendered);
382
+ }
383
+ return map;
384
+ }
312
385
  function processPresentation(document, options) {
313
386
  const { props, children = [] } = document;
314
387
  const baseTheme = typeof props.theme === "object" && props.theme !== null ? props.theme : options?.customThemes?.[props.theme ?? "default"] ?? getPptxTheme(props.theme ?? "default");
@@ -322,34 +395,41 @@ function processPresentation(document, options) {
322
395
  } : baseTheme;
323
396
  const slideWidth = props.slideWidth ?? 10;
324
397
  const slideHeight = props.slideHeight ?? 7.5;
398
+ const slideIndexMap = buildSlideIndexMap(children);
325
399
  let templates;
326
400
  if (props.templates && props.templates.length > 0) {
327
401
  templates = props.templates.map((m) => {
328
402
  const effectiveGrid = mergeGridConfigs(props.grid, m.grid);
329
403
  const resolvedPhs = m.placeholders?.map((ph) => {
330
- if (!ph.grid) return ph;
404
+ const phDefaults = ph.defaults;
405
+ const defaultProps = phDefaults?.props ? remapHyperlinkProps(phDefaults.props, slideIndexMap) : void 0;
406
+ const base = phDefaults && defaultProps && defaultProps !== phDefaults.props ? { ...ph, defaults: { ...phDefaults, props: defaultProps } } : ph;
407
+ if (!base.grid) return base;
331
408
  const abs = resolveGridPosition(
332
- ph.grid,
409
+ base.grid,
333
410
  effectiveGrid,
334
411
  slideWidth,
335
412
  slideHeight
336
413
  );
337
414
  return {
338
- ...ph,
339
- x: ph.x ?? abs.x,
340
- y: ph.y ?? abs.y,
341
- w: ph.w ?? abs.w,
342
- h: ph.h ?? abs.h,
415
+ ...base,
416
+ x: base.x ?? abs.x,
417
+ y: base.y ?? abs.y,
418
+ w: base.w ?? abs.w,
419
+ h: base.h ?? abs.h,
343
420
  grid: void 0
344
421
  };
345
422
  });
346
423
  const defaultedObjects = m.objects ? resolveComponentTree(m.objects, theme) : void 0;
347
424
  const resolvedObjects = defaultedObjects?.map(
348
- (obj) => resolveComponentGridPosition(
349
- obj,
350
- effectiveGrid,
351
- slideWidth,
352
- slideHeight
425
+ (obj) => remapHyperlinkSlideRefs(
426
+ resolveComponentGridPosition(
427
+ obj,
428
+ effectiveGrid,
429
+ slideWidth,
430
+ slideHeight
431
+ ),
432
+ slideIndexMap
353
433
  )
354
434
  );
355
435
  return { ...m, placeholders: resolvedPhs, objects: resolvedObjects };
@@ -358,13 +438,18 @@ function processPresentation(document, options) {
358
438
  const slides = [];
359
439
  for (const child of children) {
360
440
  if (isSlideComponent(child)) {
441
+ if (!isSlideEnabled(child)) continue;
361
442
  const slideComponents = [];
362
443
  if (child.children) {
363
444
  for (const slideChild of child.children) {
364
445
  slideComponents.push(slideChild);
365
446
  }
366
447
  }
367
- const resolvedComponents = resolveComponentTree(slideComponents, theme);
448
+ const resolvedComponents = resolveComponentTree(
449
+ slideComponents,
450
+ theme
451
+ ).map((component) => remapHyperlinkSlideRefs(component, slideIndexMap));
452
+ const placeholders = child.props.placeholders;
368
453
  slides.push({
369
454
  components: resolvedComponents,
370
455
  background: child.props.background,
@@ -372,7 +457,12 @@ function processPresentation(document, options) {
372
457
  layout: child.props.layout,
373
458
  hidden: child.props.hidden,
374
459
  template: child.props.template,
375
- placeholders: child.props.placeholders
460
+ placeholders: placeholders ? Object.fromEntries(
461
+ Object.entries(placeholders).map(([name, component]) => [
462
+ name,
463
+ remapHyperlinkSlideRefs(component, slideIndexMap)
464
+ ])
465
+ ) : void 0
376
466
  });
377
467
  }
378
468
  }
@@ -401,6 +491,7 @@ import PptxGenJS from "pptxgenjs";
401
491
 
402
492
  // src/utils/color.ts
403
493
  import { SEMANTIC_COLOR_NAMES } from "@json-to-office/shared-pptx";
494
+ import { DEFAULT_CHART_THEME_COLORS } from "@json-to-office/shared";
404
495
  var SEMANTIC_TO_THEME_KEY = {
405
496
  ...Object.fromEntries(SEMANTIC_COLOR_NAMES.map((n) => [n, n])),
406
497
  // Aliases (PowerPoint XML compat)
@@ -412,26 +503,49 @@ var SEMANTIC_TO_THEME_KEY = {
412
503
  bg1: "background",
413
504
  bg2: "background2"
414
505
  };
415
- var DEFAULT_CHART_THEME_COLORS = [
416
- "primary",
417
- "secondary",
418
- "accent",
419
- "accent4",
420
- "accent5",
421
- "accent6"
422
- ];
506
+ function chainToHex(value, theme, seen) {
507
+ const bare = value.startsWith("#") ? value.slice(1) : value;
508
+ if (/^[0-9A-Fa-f]{6}$/.test(bare)) return bare;
509
+ if (/^[0-9A-Fa-f]{3}$/.test(bare)) {
510
+ return bare[0] + bare[0] + bare[1] + bare[1] + bare[2] + bare[2];
511
+ }
512
+ const themeKey = SEMANTIC_TO_THEME_KEY[value];
513
+ if (!themeKey || seen.has(themeKey)) return void 0;
514
+ seen.add(themeKey);
515
+ const next = theme?.colors?.[themeKey];
516
+ if (typeof next !== "string" || next.length === 0) return void 0;
517
+ return chainToHex(next, theme, seen)?.toUpperCase();
518
+ }
519
+ function definedChartColorTokens(theme) {
520
+ const colors = theme?.colors;
521
+ if (!colors) return [];
522
+ return DEFAULT_CHART_THEME_COLORS.filter((token) => {
523
+ const themeKey = SEMANTIC_TO_THEME_KEY[token] ?? token;
524
+ const value = colors[themeKey];
525
+ if (typeof value !== "string" || value.length === 0) return false;
526
+ return chainToHex(value, theme, /* @__PURE__ */ new Set([themeKey])) !== void 0;
527
+ });
528
+ }
423
529
  function resolveColor(color, theme, warnings) {
424
530
  const themeKey = SEMANTIC_TO_THEME_KEY[color];
425
531
  if (themeKey) {
426
532
  const resolved = theme.colors[themeKey];
427
- if (resolved)
428
- return resolved.startsWith("#") ? resolved.slice(1) : resolved;
533
+ if (resolved) {
534
+ const hex = chainToHex(resolved, theme, /* @__PURE__ */ new Set([themeKey]));
535
+ if (hex) return hex;
536
+ warn(
537
+ warnings,
538
+ W.UNKNOWN_COLOR,
539
+ `Theme color "${themeKey}" is "${resolved}", which is not a hex color or a theme color name; falling back to primary`
540
+ );
541
+ return resolvePrimary(theme);
542
+ }
429
543
  warn(
430
544
  warnings,
431
545
  W.THEME_COLOR_FALLBACK,
432
546
  `Theme color "${themeKey}" not defined, falling back to primary`
433
547
  );
434
- return theme.colors.primary.startsWith("#") ? theme.colors.primary.slice(1) : theme.colors.primary;
548
+ return resolvePrimary(theme);
435
549
  }
436
550
  const bare = color.startsWith("#") ? color.slice(1) : color;
437
551
  if (/^[0-9A-Fa-f]{3}$/.test(bare)) {
@@ -446,6 +560,10 @@ function resolveColor(color, theme, warnings) {
446
560
  }
447
561
  return bare;
448
562
  }
563
+ function resolvePrimary(theme) {
564
+ const primary = theme.colors.primary;
565
+ return chainToHex(primary, theme, /* @__PURE__ */ new Set(["primary"])) ?? (primary.startsWith("#") ? primary.slice(1) : primary);
566
+ }
449
567
 
450
568
  // src/utils/fontAliasContext.ts
451
569
  import { synthesizeFamilyName } from "@json-to-office/shared";
@@ -541,19 +659,7 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
541
659
  opts.fill.transparency = props.fill.transparency;
542
660
  }
543
661
  }
544
- if (props.hyperlink) {
545
- if (props.hyperlink.url) {
546
- opts.hyperlink = {
547
- url: props.hyperlink.url,
548
- tooltip: props.hyperlink.tooltip
549
- };
550
- } else if (props.hyperlink.slide) {
551
- opts.hyperlink = {
552
- slide: props.hyperlink.slide,
553
- tooltip: props.hyperlink.tooltip
554
- };
555
- }
556
- }
662
+ applyHyperlink(opts, props.hyperlink, "text", warnings);
557
663
  const lineSpacing = props.lineSpacing ?? style?.lineSpacing;
558
664
  if (lineSpacing !== void 0) opts.lineSpacing = lineSpacing;
559
665
  const charSpacing = props.charSpacing ?? style?.charSpacing;
@@ -731,19 +837,7 @@ async function renderImageComponent(slide, props, theme, warnings, slideWidth =
731
837
  opacity: props.shadow.opacity ?? 0.5
732
838
  };
733
839
  }
734
- if (props.hyperlink) {
735
- if (props.hyperlink.url) {
736
- opts.hyperlink = {
737
- url: props.hyperlink.url,
738
- tooltip: props.hyperlink.tooltip
739
- };
740
- } else if (props.hyperlink.slide) {
741
- opts.hyperlink = {
742
- slide: props.hyperlink.slide,
743
- tooltip: props.hyperlink.tooltip
744
- };
745
- }
746
- }
840
+ applyHyperlink(opts, props.hyperlink, "image", warnings);
747
841
  if (props.alt) opts.altText = props.alt;
748
842
  slide.addImage(opts);
749
843
  }
@@ -1131,9 +1225,10 @@ Cause: ${error instanceof Error ? error.message : String(error)}`
1131
1225
  }
1132
1226
  function withThemeColors(props, theme, warnings) {
1133
1227
  if (!props.options || props.options.colors || !theme?.colors) return props;
1134
- const palette = DEFAULT_CHART_THEME_COLORS.map(
1228
+ const palette = definedChartColorTokens(theme).map(
1135
1229
  (token) => `#${resolveColor(token, theme, warnings)}`
1136
1230
  );
1231
+ if (palette.length === 0) return props;
1137
1232
  return {
1138
1233
  ...props,
1139
1234
  options: { ...props.options, colors: palette }
@@ -1167,7 +1262,6 @@ var CHART_TYPE_MAP = {
1167
1262
  radar: "radar",
1168
1263
  scatter: "scatter"
1169
1264
  };
1170
- var DEFAULT_THEME_COLORS = DEFAULT_CHART_THEME_COLORS;
1171
1265
  function renderChartComponent(slide, props, theme, _pptx, warnings) {
1172
1266
  const chartType = CHART_TYPE_MAP[props.type];
1173
1267
  if (!chartType) {
@@ -1214,8 +1308,12 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
1214
1308
  if (props.y !== void 0) opts.y = props.y;
1215
1309
  if (props.w !== void 0) opts.w = props.w;
1216
1310
  if (props.h !== void 0) opts.h = props.h;
1217
- const colorSources = props.chartColors ?? DEFAULT_THEME_COLORS;
1218
- opts.chartColors = colorSources.map((c) => resolveColor(c, theme, warnings));
1311
+ const colorSources = props.chartColors ?? definedChartColorTokens(theme);
1312
+ if (colorSources.length > 0) {
1313
+ opts.chartColors = colorSources.map(
1314
+ (c) => resolveColor(c, theme, warnings)
1315
+ );
1316
+ }
1219
1317
  const themeTextColor = resolveColor("text", theme, warnings);
1220
1318
  opts.titleColor = props.titleColor ? resolveColor(props.titleColor, theme, warnings) : themeTextColor;
1221
1319
  opts.legendColor = props.legendColor ? resolveColor(props.legendColor, theme, warnings) : themeTextColor;
@@ -1612,13 +1710,161 @@ async function resolveDocumentFonts(document, theme, warnings, fonts) {
1612
1710
 
1613
1711
  // src/core/generator.ts
1614
1712
  import { applyExportMode, scopedThemeName } from "@json-to-office/shared";
1615
- import { collectImageSourceConflicts } from "@json-to-office/shared-pptx";
1713
+ import {
1714
+ collectImageSourceConflicts,
1715
+ validateJsonPresentationDocument,
1716
+ validatePresentationDocument
1717
+ } from "@json-to-office/shared-pptx";
1718
+
1719
+ // src/core/packagePresentation.ts
1720
+ import JSZip from "jszip";
1721
+ var MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1722
+ var NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1723
+ var DEFAULT_GENERATED_AT = "2000-01-01T00:00:00.000Z";
1724
+ function resolveGeneratedAt(value) {
1725
+ const date = value === void 0 ? new Date(DEFAULT_GENERATED_AT) : new Date(value);
1726
+ if (Number.isNaN(date.getTime())) {
1727
+ throw new Error(`Invalid generatedAt value: ${String(value)}`);
1728
+ }
1729
+ if (date.getUTCFullYear() < 1980) {
1730
+ throw new Error(
1731
+ "generatedAt must be on or after 1980-01-01 for ZIP compatibility"
1732
+ );
1733
+ }
1734
+ return date;
1735
+ }
1736
+ function replaceCoreTimestamp(xml, tag, value) {
1737
+ const expression = new RegExp(
1738
+ `(<dcterms:${tag}\\b[^>]*>)[^<]*(</dcterms:${tag}>)`,
1739
+ "g"
1740
+ );
1741
+ return xml.replace(expression, `$1${value}$2`);
1742
+ }
1743
+ var EMBEDDED_OFFICE_PACKAGE = /\.(?:docx|pptx|xlsx|xlsm)$/i;
1744
+ function remapChartReferences(value, chartIds) {
1745
+ return value.replace(/chart(\d+)\.xml/g, (match, rawId) => {
1746
+ const id = chartIds.get(Number(rawId));
1747
+ return id === void 0 ? match : `chart${id}.xml`;
1748
+ }).replace(
1749
+ /Microsoft_Excel_Worksheet(\d+)\.xlsx/g,
1750
+ (match, rawId) => {
1751
+ const id = chartIds.get(Number(rawId));
1752
+ return id === void 0 ? match : `Microsoft_Excel_Worksheet${id}.xlsx`;
1753
+ }
1754
+ );
1755
+ }
1756
+ async function canonicalizeChartIds(zip) {
1757
+ 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);
1758
+ const chartIds = new Map(sourceIds.map((id, index) => [id, index + 1]));
1759
+ if (chartIds.size === 0) return;
1760
+ for (const [path2, entry] of Object.entries(zip.files)) {
1761
+ if (entry.dir || !path2.endsWith(".xml") && !path2.endsWith(".rels")) {
1762
+ continue;
1763
+ }
1764
+ const xml = await entry.async("string");
1765
+ const remapped = remapChartReferences(xml, chartIds);
1766
+ if (remapped !== xml) zip.file(path2, remapped);
1767
+ }
1768
+ const renames = [];
1769
+ for (const [path2, entry] of Object.entries(zip.files)) {
1770
+ if (entry.dir) continue;
1771
+ const remappedPath = remapChartReferences(path2, chartIds);
1772
+ if (remappedPath === path2) continue;
1773
+ renames.push({
1774
+ from: path2,
1775
+ to: remappedPath,
1776
+ data: await entry.async("nodebuffer"),
1777
+ date: entry.date
1778
+ });
1779
+ }
1780
+ for (const entry of renames) zip.remove(entry.from);
1781
+ for (const entry of renames) {
1782
+ zip.file(entry.to, entry.data, { date: entry.date });
1783
+ }
1784
+ }
1785
+ async function generateZip(zip) {
1786
+ return await zip.generateAsync({
1787
+ type: "nodebuffer",
1788
+ compression: "DEFLATE",
1789
+ compressionOptions: { level: 6 },
1790
+ platform: "DOS",
1791
+ streamFiles: false
1792
+ });
1793
+ }
1794
+ async function canonicalizePackage(zip, generatedAt, depth = 0) {
1795
+ const timestamp = generatedAt.toISOString().replace(/\.\d{3}Z$/, "Z");
1796
+ const coreEntry = zip.file("docProps/core.xml");
1797
+ if (coreEntry) {
1798
+ let coreXml = await coreEntry.async("string");
1799
+ coreXml = replaceCoreTimestamp(coreXml, "created", timestamp);
1800
+ coreXml = replaceCoreTimestamp(coreXml, "modified", timestamp);
1801
+ zip.file("docProps/core.xml", coreXml);
1802
+ }
1803
+ if (depth < 3) {
1804
+ for (const [path2, entry] of Object.entries(zip.files)) {
1805
+ if (entry.dir || !EMBEDDED_OFFICE_PACKAGE.test(path2)) continue;
1806
+ try {
1807
+ const nested = await JSZip.loadAsync(await entry.async("nodebuffer"));
1808
+ await canonicalizePackage(nested, generatedAt, depth + 1);
1809
+ zip.file(path2, await generateZip(nested));
1810
+ } catch {
1811
+ }
1812
+ }
1813
+ }
1814
+ for (const entry of Object.values(zip.files)) {
1815
+ entry.date = generatedAt;
1816
+ }
1817
+ }
1818
+ async function packagePresentationBuffer(buffer, options = {}) {
1819
+ const zip = await JSZip.loadAsync(buffer);
1820
+ let changed = false;
1821
+ for (const [path2, entry] of Object.entries(zip.files)) {
1822
+ if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
1823
+ const xml = await entry.async("string");
1824
+ if (xml.includes(MEDIUM_STYLE_2_ACCENT_1)) {
1825
+ zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID));
1826
+ changed = true;
1827
+ }
1828
+ }
1829
+ if (options.deterministic !== false) {
1830
+ const generatedAt = resolveGeneratedAt(options.generatedAt);
1831
+ await canonicalizeChartIds(zip);
1832
+ await canonicalizePackage(zip, generatedAt);
1833
+ changed = true;
1834
+ }
1835
+ if (!changed) return buffer;
1836
+ return generateZip(zip);
1837
+ }
1838
+
1839
+ // src/core/generator.ts
1840
+ var PresentationValidationError = class extends Error {
1841
+ errors;
1842
+ constructor(errors) {
1843
+ super(
1844
+ `Presentation validation failed:
1845
+ ${errors.map((error) => ` - ${error.path}: ${error.message}`).join("\n")}`
1846
+ );
1847
+ this.name = "PresentationValidationError";
1848
+ this.errors = errors;
1849
+ }
1850
+ };
1851
+ function assertValidPresentation(input, validation) {
1852
+ if (validation?.enabled === false) return;
1853
+ const options = {
1854
+ allowUnknownFields: validation?.allowUnknownFields
1855
+ };
1856
+ const result = typeof input === "string" ? validateJsonPresentationDocument(input, options) : validatePresentationDocument(input, options);
1857
+ if (!result.valid) {
1858
+ throw new PresentationValidationError(result.errors);
1859
+ }
1860
+ }
1616
1861
  function isPresentationComponentDefinition(definition) {
1617
1862
  if (typeof definition !== "object" || definition === null) return false;
1618
1863
  const def = definition;
1619
1864
  return def.name === "pptx" && "props" in def;
1620
1865
  }
1621
1866
  async function generatePresentation(document, options, warnings) {
1867
+ assertValidPresentation(document, options?.validation);
1622
1868
  if (!document || document.name !== "pptx") {
1623
1869
  throw new Error("Top-level component must be a pptx component");
1624
1870
  }
@@ -1637,6 +1883,7 @@ async function generateBufferFromJson(jsonConfig, options) {
1637
1883
  return result.buffer;
1638
1884
  }
1639
1885
  async function generateBufferWithWarnings(jsonConfig, options) {
1886
+ assertValidPresentation(jsonConfig, options?.validation);
1640
1887
  let component;
1641
1888
  if (typeof jsonConfig === "string") {
1642
1889
  const parsed = JSON.parse(jsonConfig);
@@ -1705,36 +1952,21 @@ async function generateBufferWithWarnings(jsonConfig, options) {
1705
1952
  warnings
1706
1953
  );
1707
1954
  const data = await pptx.write({ outputType: "nodebuffer" });
1708
- const buffer = await neutralizeTableStyle(data);
1955
+ const buffer = await packagePresentationBuffer(data, options);
1709
1956
  return { buffer, warnings };
1710
1957
  }
1711
1958
  async function generateAndSaveFromJson(jsonConfig, outputPath, options) {
1712
1959
  const buffer = await generateBufferFromJson(jsonConfig, options);
1713
1960
  writeFileSync(outputPath, buffer);
1714
1961
  }
1715
- async function generateFromFile(filePath, outputPath) {
1962
+ async function generateFromFile(filePath, outputPath, options) {
1716
1963
  const { readFileSync } = await import("fs");
1717
1964
  const json = readFileSync(filePath, "utf-8");
1718
- await generateAndSaveFromJson(json, outputPath);
1719
- }
1720
- var MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1721
- var NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1722
- async function neutralizeTableStyle(buffer) {
1723
- const zip = await JSZip.loadAsync(buffer);
1724
- let changed = false;
1725
- for (const [path2, entry] of Object.entries(zip.files)) {
1726
- if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
1727
- const xml = await entry.async("string");
1728
- if (xml.includes(MEDIUM_STYLE_2_ACCENT_1)) {
1729
- zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID));
1730
- changed = true;
1731
- }
1732
- }
1733
- return changed ? await zip.generateAsync({ type: "nodebuffer" }) : buffer;
1965
+ await generateAndSaveFromJson(json, outputPath, options);
1734
1966
  }
1735
- async function savePresentation(pptx, outputPath) {
1967
+ async function savePresentation(pptx, outputPath, options) {
1736
1968
  const data = await pptx.write({ outputType: "nodebuffer" });
1737
- const buffer = await neutralizeTableStyle(data);
1969
+ const buffer = await packagePresentationBuffer(data, options);
1738
1970
  writeFileSync(outputPath, buffer);
1739
1971
  }
1740
1972
  var PresentationGenerator = {
@@ -1754,7 +1986,6 @@ import {
1754
1986
  } from "@json-to-office/shared/plugin";
1755
1987
 
1756
1988
  // src/plugin/createPresentationGenerator.ts
1757
- import JSZip2 from "jszip";
1758
1989
  import {
1759
1990
  resolveComponentVersion as resolveComponentVersion2,
1760
1991
  DuplicateComponentError as DuplicateComponentError2,
@@ -1767,23 +1998,33 @@ import {
1767
1998
  validateCustomComponentProps,
1768
1999
  ComponentValidationError
1769
2000
  } from "@json-to-office/shared/plugin";
1770
- import { collectImageSourceConflicts as collectImageSourceConflicts2 } from "@json-to-office/shared-pptx";
2001
+ import { validatePresentationDocument as validatePresentationDocument2 } from "@json-to-office/shared-pptx";
1771
2002
  import {
1772
2003
  DuplicateComponentError,
1773
2004
  ComponentValidationError as ComponentValidationError2
1774
2005
  } from "@json-to-office/shared/plugin";
1775
- function validateComponentProps(schema, props, componentName) {
2006
+ function validateComponentProps(schema, props, componentName, opts) {
1776
2007
  return validateCustomComponentProps(schema.propsSchema, props, {
1777
- clean: true,
1778
- applyDefaults: true,
2008
+ // Render-time cleaning remains the default. The document-validation path
2009
+ // passes clean:false so unknown custom props are rejected when the custom
2010
+ // schema declares additionalProperties:false.
2011
+ clean: opts?.clean ?? true,
2012
+ applyDefaults: opts?.applyDefaults ?? true,
1779
2013
  componentName
1780
2014
  });
1781
2015
  }
1782
- function validatePresentation(document, customComponents) {
1783
- const errors = [];
1784
- errors.push(...collectImageSourceConflicts2(document));
2016
+ function validatePresentation(document, customComponents, options) {
2017
+ const knownCustomNames = new Set(customComponents.map((c) => c.name));
2018
+ const documentResult = validatePresentationDocument2(document, {
2019
+ knownCustomNames,
2020
+ allowUnknownFields: options?.allowUnknownFields
2021
+ });
2022
+ const errors = [...documentResult.errors];
1785
2023
  function validateComponents(components, pathPrefix = "children") {
1786
2024
  components.forEach((componentData, index) => {
2025
+ if (!componentData || typeof componentData !== "object" || Array.isArray(componentData)) {
2026
+ return;
2027
+ }
1787
2028
  const customComponent = customComponents.find(
1788
2029
  (cc) => cc.name === componentData.name
1789
2030
  );
@@ -1796,7 +2037,8 @@ function validatePresentation(document, customComponents) {
1796
2037
  const validation = validateComponentProps(
1797
2038
  versionEntry,
1798
2039
  componentData.props,
1799
- customComponent.name
2040
+ customComponent.name,
2041
+ { clean: options?.allowUnknownFields === true }
1800
2042
  );
1801
2043
  if (!validation.valid && validation.errors) {
1802
2044
  const indexedErrors = validation.errors.map(
@@ -1816,7 +2058,7 @@ function validatePresentation(document, customComponents) {
1816
2058
  }
1817
2059
  });
1818
2060
  }
1819
- if (document.children) {
2061
+ if (document && Array.isArray(document.children)) {
1820
2062
  validateComponents(document.children);
1821
2063
  }
1822
2064
  return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [] };
@@ -1866,24 +2108,9 @@ async function exportPluginSchema(customComponents, outputPath, options = {}) {
1866
2108
 
1867
2109
  // src/plugin/createPresentationGenerator.ts
1868
2110
  import { applyExportMode as applyExportMode2, scopedThemeName as scopedThemeName2 } from "@json-to-office/shared";
1869
- var MEDIUM_STYLE_2_ACCENT_12 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1870
- var NO_STYLE_NO_GRID2 = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1871
- async function neutralizeTableStyle2(buffer) {
1872
- const zip = await JSZip2.loadAsync(buffer);
1873
- let changed = false;
1874
- for (const [path2, entry] of Object.entries(zip.files)) {
1875
- if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
1876
- const xml = await entry.async("string");
1877
- if (xml.includes(MEDIUM_STYLE_2_ACCENT_12)) {
1878
- zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_12, NO_STYLE_NO_GRID2));
1879
- changed = true;
1880
- }
1881
- }
1882
- return changed ? await zip.generateAsync({ type: "nodebuffer" }) : buffer;
1883
- }
1884
2111
  function createBuilderImpl(state) {
1885
2112
  const componentMap = new Map(state.components.map((c) => [c.name, c]));
1886
- async function processSlideComponents(components, warningsCollector, theme, depth = 0) {
2113
+ async function processSlideComponents(components, warningsCollector, theme, validateEmitted, parentName, depth = 0) {
1887
2114
  if (depth > 20) {
1888
2115
  throw new Error(
1889
2116
  "Maximum component nesting depth exceeded (20). Check for circular component references."
@@ -1915,6 +2142,8 @@ function createBuilderImpl(state) {
1915
2142
  componentWithVersion.children,
1916
2143
  warningsCollector,
1917
2144
  theme,
2145
+ validateEmitted,
2146
+ void 0,
1918
2147
  depth + 1
1919
2148
  );
1920
2149
  }
@@ -1934,10 +2163,13 @@ function createBuilderImpl(state) {
1934
2163
  children: nestedChildren
1935
2164
  });
1936
2165
  const resultComponents = Array.isArray(result) ? result : [result];
2166
+ validateEmitted?.(resultComponents, versionLabel, parentName);
1937
2167
  const processedResult = await processSlideComponents(
1938
2168
  resultComponents,
1939
2169
  warningsCollector,
1940
2170
  theme,
2171
+ validateEmitted,
2172
+ parentName,
1941
2173
  depth + 1
1942
2174
  );
1943
2175
  processed.push(...processedResult);
@@ -1961,6 +2193,8 @@ function createBuilderImpl(state) {
1961
2193
  componentData.children,
1962
2194
  warningsCollector,
1963
2195
  theme,
2196
+ validateEmitted,
2197
+ componentData.name,
1964
2198
  depth + 1
1965
2199
  );
1966
2200
  processed.push({
@@ -1990,16 +2224,31 @@ function createBuilderImpl(state) {
1990
2224
  customThemes: state.customThemes,
1991
2225
  debug: state.debug,
1992
2226
  services: state.services,
1993
- fonts: state.fonts
2227
+ fonts: state.fonts,
2228
+ validation: state.validation,
2229
+ packaging: state.packaging
1994
2230
  };
1995
2231
  return createBuilderImpl(
1996
2232
  newState
1997
2233
  );
1998
2234
  }
1999
- async function generate(document) {
2235
+ async function generate(document, options) {
2000
2236
  try {
2001
2237
  let internalDocument = document;
2002
- if (!internalDocument || internalDocument.name !== "pptx") {
2238
+ const validationOptions = {
2239
+ ...state.validation,
2240
+ ...options?.validation
2241
+ };
2242
+ if (validationOptions.enabled !== false) {
2243
+ const result = validatePresentation(
2244
+ internalDocument,
2245
+ state.components,
2246
+ { allowUnknownFields: validationOptions.allowUnknownFields }
2247
+ );
2248
+ if (!result.valid) {
2249
+ throw new ComponentValidationError3(result.errors, internalDocument);
2250
+ }
2251
+ } else if (!internalDocument || internalDocument.name !== "pptx") {
2003
2252
  throw new Error("Top-level component must be a pptx component");
2004
2253
  }
2005
2254
  let inlineTheme;
@@ -2030,7 +2279,39 @@ function createBuilderImpl(state) {
2030
2279
  component: "fontRegistry"
2031
2280
  });
2032
2281
  }
2033
- const processedChildren = mode.doc.children ? await processAllSlides(mode.doc.children, warnings, resolvedTheme) : [];
2282
+ const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
2283
+ let validationDocument;
2284
+ if (parentName === "pptx") {
2285
+ validationDocument = { ...mode.doc, children: emitted };
2286
+ } else if (parentName === "slide") {
2287
+ validationDocument = {
2288
+ ...mode.doc,
2289
+ children: [{ name: "slide", props: {}, children: emitted }]
2290
+ };
2291
+ } else {
2292
+ return;
2293
+ }
2294
+ const result = validatePresentation(
2295
+ validationDocument,
2296
+ state.components,
2297
+ { allowUnknownFields: validationOptions.allowUnknownFields }
2298
+ );
2299
+ if (!result.valid) {
2300
+ throw new ComponentValidationError3(
2301
+ result.errors.map((error) => ({
2302
+ ...error,
2303
+ message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
2304
+ })),
2305
+ emitted
2306
+ );
2307
+ }
2308
+ };
2309
+ const processedChildren = mode.doc.children ? await processAllSlides(
2310
+ mode.doc.children,
2311
+ warnings,
2312
+ resolvedTheme,
2313
+ validateEmitted
2314
+ ) : [];
2034
2315
  const themeName = scopedThemeName2(baseThemeName, state.fonts?.mode);
2035
2316
  const docWithScopedTheme = themeName !== baseThemeName ? {
2036
2317
  ...mode.doc,
@@ -2038,6 +2319,20 @@ function createBuilderImpl(state) {
2038
2319
  children: processedChildren
2039
2320
  } : { ...mode.doc, children: processedChildren };
2040
2321
  const processedDocument = docWithScopedTheme;
2322
+ if (validationOptions.enabled !== false) {
2323
+ const result = validatePresentation(processedDocument, [], {
2324
+ allowUnknownFields: validationOptions.allowUnknownFields
2325
+ });
2326
+ if (!result.valid) {
2327
+ throw new ComponentValidationError3(
2328
+ result.errors.map((error) => ({
2329
+ ...error,
2330
+ message: `expanded plugin output failed validation \u2014 ${error.message}`
2331
+ })),
2332
+ processedDocument
2333
+ );
2334
+ }
2335
+ }
2041
2336
  await resolveDocumentFonts(
2042
2337
  processedDocument,
2043
2338
  resolvedTheme,
@@ -2054,7 +2349,10 @@ function createBuilderImpl(state) {
2054
2349
  });
2055
2350
  const pptx = await renderPresentation(processed, warnings);
2056
2351
  const data = await pptx.write({ outputType: "nodebuffer" });
2057
- const buffer = await neutralizeTableStyle2(data);
2352
+ const buffer = await packagePresentationBuffer(data, {
2353
+ deterministic: options?.deterministic ?? state.packaging.deterministic,
2354
+ generatedAt: options?.generatedAt ?? state.packaging.generatedAt
2355
+ });
2058
2356
  return { buffer, warnings };
2059
2357
  } catch (error) {
2060
2358
  if (state.debug) {
@@ -2063,29 +2361,33 @@ function createBuilderImpl(state) {
2063
2361
  throw error;
2064
2362
  }
2065
2363
  }
2066
- async function processAllSlides(children, warnings, theme) {
2364
+ async function processAllSlides(children, warnings, theme, validateEmitted) {
2067
2365
  const result = [];
2068
2366
  for (const child of children) {
2069
2367
  if (child.name === "slide" && child.children) {
2070
2368
  const processedSlideChildren = await processSlideComponents(
2071
2369
  child.children,
2072
2370
  warnings,
2073
- theme
2371
+ theme,
2372
+ validateEmitted,
2373
+ "slide"
2074
2374
  );
2075
2375
  result.push({ ...child, children: processedSlideChildren });
2076
2376
  } else {
2077
2377
  const processedTopLevel = await processSlideComponents(
2078
2378
  [child],
2079
2379
  warnings,
2080
- theme
2380
+ theme,
2381
+ validateEmitted,
2382
+ "pptx"
2081
2383
  );
2082
2384
  result.push(...processedTopLevel);
2083
2385
  }
2084
2386
  }
2085
2387
  return result;
2086
2388
  }
2087
- async function generateFile(document, outputPath) {
2088
- const { buffer, warnings } = await generate(document);
2389
+ async function generateFile(document, outputPath, options) {
2390
+ const { buffer, warnings } = await generate(document, options);
2089
2391
  const fs = await import("fs/promises");
2090
2392
  await fs.writeFile(outputPath, new Uint8Array(buffer));
2091
2393
  return { warnings };
@@ -2162,7 +2464,12 @@ function createPresentationGenerator(options = {}) {
2162
2464
  customThemes: options.customThemes,
2163
2465
  debug: options.debug ?? false,
2164
2466
  services: options.services,
2165
- fonts: options.fonts
2467
+ fonts: options.fonts,
2468
+ validation: options.validation,
2469
+ packaging: {
2470
+ deterministic: options.deterministic,
2471
+ generatedAt: options.generatedAt
2472
+ }
2166
2473
  };
2167
2474
  return createBuilderImpl(initialState);
2168
2475
  }
@@ -2176,9 +2483,11 @@ function getPptxCoreVersion() {
2176
2483
  }
2177
2484
  export {
2178
2485
  ComponentValidationError2 as ComponentValidationError,
2486
+ DEFAULT_GENERATED_AT,
2179
2487
  DEFAULT_PPTX_THEME,
2180
2488
  DuplicateComponentError,
2181
2489
  PresentationGenerator,
2490
+ PresentationValidationError,
2182
2491
  W as WarningCodes,
2183
2492
  cleanComponentProps,
2184
2493
  createComponent,
@@ -2196,6 +2505,7 @@ export {
2196
2505
  isPresentationComponent,
2197
2506
  isPresentationComponentDefinition,
2198
2507
  isSlideComponent,
2508
+ packagePresentationBuffer,
2199
2509
  pptxThemes,
2200
2510
  renderComponent,
2201
2511
  renderHighchartsComponent,