@json-to-office/core-pptx 0.24.0 → 0.26.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
@@ -25,6 +25,9 @@ var W = {
25
25
  THEME_COLOR_FALLBACK: "THEME_COLOR_FALLBACK",
26
26
  UNKNOWN_COLOR: "UNKNOWN_COLOR",
27
27
  GRID_POSITION_CLAMPED: "GRID_POSITION_CLAMPED",
28
+ TEXT_NO_CONTENT: "TEXT_NO_CONTENT",
29
+ UNKNOWN_PATTERN_PRESET: "UNKNOWN_PATTERN_PRESET",
30
+ ADVANCED_FILL_FALLBACK: "ADVANCED_FILL_FALLBACK",
28
31
  IMAGE_ZERO_BOX: "IMAGE_ZERO_BOX",
29
32
  FONT_UNRESOLVED: "FONT_UNRESOLVED"
30
33
  };
@@ -384,7 +387,7 @@ function buildSlideIndexMap(children) {
384
387
  }
385
388
  function processPresentation(document, options) {
386
389
  const { props, children = [] } = document;
387
- const baseTheme = typeof props.theme === "object" && props.theme !== null ? props.theme : options?.customThemes?.[props.theme ?? "default"] ?? getPptxTheme(props.theme ?? "default");
390
+ const baseTheme = options?.theme ?? (typeof props.theme === "object" && props.theme !== null ? props.theme : options?.customThemes?.[props.theme ?? "default"] ?? getPptxTheme(props.theme ?? "default"));
388
391
  const presDefaults = props.componentDefaults;
389
392
  const theme = presDefaults ? {
390
393
  ...baseTheme,
@@ -591,6 +594,16 @@ function resolvePagePlaceholders(text, ctx) {
591
594
  return text.replace(/\{PAGE_NUMBER\}/g, fmt(slideNumber)).replace(/\{PAGE_COUNT\}/g, fmt(totalSlides));
592
595
  }
593
596
  function renderTextComponent(slide, props, theme, warnings, slideCtx) {
597
+ const runs = props.runs && props.runs.length > 0 ? props.runs : void 0;
598
+ if (props.text === void 0 && !runs) {
599
+ warn(
600
+ warnings,
601
+ W.TEXT_NO_CONTENT,
602
+ 'Text component has neither "text" nor "runs" \u2014 skipped',
603
+ { component: "text" }
604
+ );
605
+ return;
606
+ }
594
607
  const style = props.style ? theme.styles?.[props.style] : void 0;
595
608
  const isHeadingStyle = props.style && /^(title|heading)/.test(props.style);
596
609
  const opts = {};
@@ -600,7 +613,10 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
600
613
  if (props.h !== void 0) opts.h = props.h;
601
614
  if (props.h === void 0) {
602
615
  const fontSize = props.fontSize ?? theme.defaults.fontSize ?? 18;
603
- const lines = (props.text.match(/\n/g)?.length ?? 0) + 1;
616
+ const lines = runs ? runs.reduce(
617
+ (count, run) => count + (run.breakLine ? 1 : 0) + (run.text.match(/\n/g)?.length ?? 0),
618
+ 1
619
+ ) : (props.text.match(/\n/g)?.length ?? 0) + 1;
604
620
  opts.h = Math.max(0.5, fontSize / 72 * 1.6 * lines);
605
621
  opts.isTextBox = true;
606
622
  }
@@ -611,6 +627,7 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
611
627
  theme,
612
628
  warnings
613
629
  );
630
+ const preAliasFamily = opts.fontFace;
614
631
  const bold = props.bold ?? style?.bold;
615
632
  const italic = props.italic ?? style?.italic;
616
633
  const fontWeight = props.fontWeight ?? style?.fontWeight;
@@ -661,7 +678,11 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
661
678
  }
662
679
  applyHyperlink(opts, props.hyperlink, "text", warnings);
663
680
  const lineSpacing = props.lineSpacing ?? style?.lineSpacing;
664
- if (lineSpacing !== void 0) opts.lineSpacing = lineSpacing;
681
+ if (props.lineSpacingMultiple !== void 0) {
682
+ opts.lineSpacingMultiple = props.lineSpacingMultiple;
683
+ } else if (lineSpacing !== void 0) {
684
+ opts.lineSpacing = lineSpacing;
685
+ }
665
686
  const charSpacing = props.charSpacing ?? style?.charSpacing;
666
687
  if (charSpacing !== void 0) opts.charSpacing = charSpacing;
667
688
  if (props.paraSpaceBefore !== void 0)
@@ -669,6 +690,49 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
669
690
  const paraSpaceAfter = props.paraSpaceAfter ?? style?.paraSpaceAfter;
670
691
  if (paraSpaceAfter !== void 0) opts.paraSpaceAfter = paraSpaceAfter;
671
692
  if (props.breakLine) opts.breakLine = true;
693
+ if (runs) {
694
+ const runSegments = runs.map((run) => {
695
+ const runOpts = {};
696
+ if (run.fontSize != null) runOpts.fontSize = run.fontSize;
697
+ if (run.fontFace != null) runOpts.fontFace = run.fontFace;
698
+ if (run.color != null)
699
+ runOpts.color = resolveColor(run.color, theme, warnings);
700
+ if (run.strike != null) runOpts.strike = run.strike;
701
+ if (run.underline !== void 0) {
702
+ if (typeof run.underline === "boolean") {
703
+ if (run.underline) runOpts.underline = { style: "sng" };
704
+ } else {
705
+ runOpts.underline = run.underline;
706
+ }
707
+ }
708
+ if (run.superscript != null) runOpts.superscript = run.superscript;
709
+ if (run.subscript != null) runOpts.subscript = run.subscript;
710
+ if (run.charSpacing != null) runOpts.charSpacing = run.charSpacing;
711
+ if (run.breakLine != null) runOpts.breakLine = run.breakLine;
712
+ const effWeight = run.fontWeight ?? fontWeight;
713
+ const effBold = run.bold ?? bold;
714
+ const effItalic = run.italic ?? italic;
715
+ if (effBold != null) runOpts.bold = effBold;
716
+ if (effItalic != null) runOpts.italic = effItalic;
717
+ if (effWeight != null || effBold === true) {
718
+ if (run.fontFace == null) {
719
+ const w = applyFontWeight({
720
+ family: preAliasFamily,
721
+ fontWeight: effWeight,
722
+ italic: effItalic,
723
+ bold: effBold
724
+ });
725
+ if (w.fontFace !== void 0) runOpts.fontFace = w.fontFace;
726
+ if (w.bold !== void 0) runOpts.bold = w.bold;
727
+ if (w.italic !== void 0) runOpts.italic = w.italic;
728
+ }
729
+ }
730
+ const runText = slideCtx ? resolvePagePlaceholders(run.text, slideCtx) : run.text;
731
+ return { text: runText, options: runOpts };
732
+ });
733
+ slide.addText(runSegments, opts);
734
+ return;
735
+ }
672
736
  const text = slideCtx ? resolvePagePlaceholders(props.text, slideCtx) : props.text;
673
737
  slide.addText(text, opts);
674
738
  }
@@ -842,6 +906,44 @@ async function renderImageComponent(slide, props, theme, warnings, slideWidth =
842
906
  slide.addImage(opts);
843
907
  }
844
908
 
909
+ // src/components/shape.ts
910
+ import { PATTERN_FILL_PRESETS } from "@json-to-office/shared-pptx";
911
+
912
+ // src/utils/fillXml.ts
913
+ var ANGLE_UNIT = 6e4;
914
+ var PCT_UNIT = 1e3;
915
+ var RADIAL_FOCUS_RECTS = {
916
+ center: { l: 5e4, t: 5e4, r: 5e4, b: 5e4 },
917
+ topLeft: { l: 0, t: 0, r: 1e5, b: 1e5 },
918
+ topRight: { l: 1e5, t: 0, r: 0, b: 1e5 },
919
+ bottomLeft: { l: 0, t: 1e5, r: 1e5, b: 0 },
920
+ bottomRight: { l: 1e5, t: 1e5, r: 0, b: 0 }
921
+ };
922
+ function gradientStopXml(color, pos, transparency, theme, warnings) {
923
+ const hex = resolveColor(color, theme, warnings).toUpperCase();
924
+ const alpha = transparency !== void 0 ? `<a:alpha val="${Math.round((100 - transparency) * PCT_UNIT)}"/>` : "";
925
+ return `<a:gs pos="${Math.round(pos * PCT_UNIT)}"><a:srgbClr val="${hex}">${alpha}</a:srgbClr></a:gs>`;
926
+ }
927
+ function buildGradientFillXml(gradient, theme, warnings) {
928
+ const stops = gradient.stops.map(
929
+ (stop) => gradientStopXml(stop.color, stop.pos, stop.transparency, theme, warnings)
930
+ ).join("");
931
+ let shade;
932
+ if (gradient.type === "radial") {
933
+ const rect = RADIAL_FOCUS_RECTS[gradient.focus ?? "center"];
934
+ shade = `<a:path path="circle"><a:fillToRect l="${rect.l}" t="${rect.t}" r="${rect.r}" b="${rect.b}"/></a:path>`;
935
+ } else {
936
+ const angle = ((gradient.angle ?? 0) % 360 + 360) % 360;
937
+ shade = `<a:lin ang="${Math.round(angle * ANGLE_UNIT)}" scaled="1"/>`;
938
+ }
939
+ return `<a:gradFill rotWithShape="1"><a:gsLst>${stops}</a:gsLst>${shade}</a:gradFill>`;
940
+ }
941
+ function buildPatternFillXml(pattern, theme, warnings) {
942
+ const fg = resolveColor(pattern.foreground, theme, warnings).toUpperCase();
943
+ const bg = resolveColor(pattern.background, theme, warnings).toUpperCase();
944
+ return `<a:pattFill prst="${pattern.preset}"><a:fgClr><a:srgbClr val="${fg}"/></a:fgClr><a:bgClr><a:srgbClr val="${bg}"/></a:bgClr></a:pattFill>`;
945
+ }
946
+
845
947
  // src/components/shape.ts
846
948
  var SHAPE_TYPE_MAP = {
847
949
  rect: "rect",
@@ -860,17 +962,67 @@ var SHAPE_TYPE_MAP = {
860
962
  heart: "heart",
861
963
  lightning: "lightningBolt"
862
964
  };
863
- function buildShapeOpts(props, theme, warnings) {
965
+ function applyShapeFill(opts, fill, theme, warnings, pendingFills) {
966
+ let gradient = fill.gradient;
967
+ let pattern = fill.pattern;
968
+ if (gradient && pattern) {
969
+ warn(
970
+ warnings,
971
+ W.ADVANCED_FILL_FALLBACK,
972
+ 'Shape fill sets both "gradient" and "pattern" \u2014 using the gradient',
973
+ { component: "shape" }
974
+ );
975
+ pattern = void 0;
976
+ }
977
+ let unknownPresetForeground;
978
+ if (pattern && !PATTERN_FILL_PRESETS.includes(pattern.preset)) {
979
+ warn(
980
+ warnings,
981
+ W.UNKNOWN_PATTERN_PRESET,
982
+ `Unknown pattern preset "${pattern.preset}" \u2014 falling back to solid foreground`,
983
+ { component: "shape" }
984
+ );
985
+ unknownPresetForeground = pattern.foreground;
986
+ pattern = void 0;
987
+ }
988
+ if (gradient || pattern) {
989
+ const sentinel = resolveColor(
990
+ fill.color ?? (gradient ? gradient.stops[0].color : pattern.foreground),
991
+ theme,
992
+ warnings
993
+ );
994
+ if (pendingFills) {
995
+ const xml = gradient ? buildGradientFillXml(gradient, theme, warnings) : buildPatternFillXml(pattern, theme, warnings);
996
+ const objectName = `__jto_fill_${pendingFills.length}__`;
997
+ pendingFills.push({ objectName, xml });
998
+ opts.objectName = objectName;
999
+ } else {
1000
+ warn(
1001
+ warnings,
1002
+ W.ADVANCED_FILL_FALLBACK,
1003
+ `${gradient ? "Gradient" : "Pattern"} fill requires the buffer generation pipeline \u2014 rendering a solid fill instead`,
1004
+ { component: "shape" }
1005
+ );
1006
+ }
1007
+ opts.fill = { color: sentinel };
1008
+ return;
1009
+ }
1010
+ const solid = fill.color ?? unknownPresetForeground;
1011
+ if (solid !== void 0) {
1012
+ opts.fill = { color: resolveColor(solid, theme, warnings) };
1013
+ if (fill.transparency !== void 0) {
1014
+ opts.fill.transparency = fill.transparency;
1015
+ }
1016
+ }
1017
+ }
1018
+ function buildShapeOpts(props, theme, warnings, pendingFills) {
864
1019
  const opts = {};
865
1020
  if (props.x !== void 0) opts.x = props.x;
866
1021
  if (props.y !== void 0) opts.y = props.y;
867
1022
  if (props.w !== void 0) opts.w = props.w;
868
1023
  if (props.h !== void 0) opts.h = props.h;
869
1024
  if (props.fill) {
870
- opts.fill = { color: resolveColor(props.fill.color, theme, warnings) };
871
- if (props.fill.transparency !== void 0) {
872
- opts.fill.transparency = props.fill.transparency;
873
- }
1025
+ applyShapeFill(opts, props.fill, theme, warnings, pendingFills);
874
1026
  }
875
1027
  if (props.line) {
876
1028
  opts.line = {};
@@ -886,6 +1038,9 @@ function buildShapeOpts(props, theme, warnings) {
886
1038
  opts.line.dashType = props.line.dashType;
887
1039
  }
888
1040
  if (props.rotate !== void 0) opts.rotate = props.rotate;
1041
+ if (props.angleRange !== void 0) opts.angleRange = props.angleRange;
1042
+ if (props.flipH !== void 0) opts.flipH = props.flipH;
1043
+ if (props.flipV !== void 0) opts.flipV = props.flipV;
889
1044
  if (props.rectRadius !== void 0) opts.rectRadius = props.rectRadius;
890
1045
  if (props.shadow) {
891
1046
  opts.shadow = {
@@ -899,7 +1054,7 @@ function buildShapeOpts(props, theme, warnings) {
899
1054
  }
900
1055
  return opts;
901
1056
  }
902
- function renderShapeComponent(slide, props, theme, pptx, warnings) {
1057
+ function renderShapeComponent(slide, props, theme, pptx, warnings, ctx) {
903
1058
  const shapeTypeName = SHAPE_TYPE_MAP[props.type] || props.type;
904
1059
  const shapeType = pptx.ShapeType[shapeTypeName];
905
1060
  if (!shapeType) {
@@ -910,7 +1065,7 @@ function renderShapeComponent(slide, props, theme, pptx, warnings) {
910
1065
  }
911
1066
  const style = props.style ? theme.styles?.[props.style] : void 0;
912
1067
  const isHeadingStyle = props.style && /^(title|heading)/.test(props.style);
913
- const opts = buildShapeOpts(props, theme, warnings);
1068
+ const opts = buildShapeOpts(props, theme, warnings, ctx?.pendingFills);
914
1069
  if (props.text && (!Array.isArray(props.text) || props.text.length > 0)) {
915
1070
  opts.shape = shapeType;
916
1071
  opts.fontSize = props.fontSize ?? style?.fontSize ?? theme.defaults.fontSize;
@@ -1262,6 +1417,14 @@ var CHART_TYPE_MAP = {
1262
1417
  radar: "radar",
1263
1418
  scatter: "scatter"
1264
1419
  };
1420
+ function resolveGridLine(gridLine, theme, warnings) {
1421
+ const resolved = {};
1422
+ if (gridLine.style !== void 0) resolved.style = gridLine.style;
1423
+ if (gridLine.size !== void 0) resolved.size = gridLine.size;
1424
+ if (gridLine.color !== void 0)
1425
+ resolved.color = resolveColor(gridLine.color, theme, warnings);
1426
+ return resolved;
1427
+ }
1265
1428
  function renderChartComponent(slide, props, theme, _pptx, warnings) {
1266
1429
  const chartType = CHART_TYPE_MAP[props.type];
1267
1430
  if (!chartType) {
@@ -1319,6 +1482,18 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
1319
1482
  opts.legendColor = props.legendColor ? resolveColor(props.legendColor, theme, warnings) : themeTextColor;
1320
1483
  opts.catAxisLabelColor = props.catAxisLabelColor ? resolveColor(props.catAxisLabelColor, theme, warnings) : themeTextColor;
1321
1484
  opts.valAxisLabelColor = props.valAxisLabelColor ? resolveColor(props.valAxisLabelColor, theme, warnings) : themeTextColor;
1485
+ if (props.valAxisLabelFontSize !== void 0)
1486
+ opts.valAxisLabelFontSize = props.valAxisLabelFontSize;
1487
+ if (props.catAxisLineShow !== void 0)
1488
+ opts.catAxisLineShow = props.catAxisLineShow;
1489
+ if (props.valAxisLineShow !== void 0)
1490
+ opts.valAxisLineShow = props.valAxisLineShow;
1491
+ if (props.dataBorder !== void 0) {
1492
+ opts.dataBorder = {
1493
+ pt: props.dataBorder.pt,
1494
+ color: resolveColor(props.dataBorder.color, theme, warnings)
1495
+ };
1496
+ }
1322
1497
  if (props.showLegend !== void 0) opts.showLegend = props.showLegend;
1323
1498
  if (props.showTitle !== void 0) opts.showTitle = props.showTitle;
1324
1499
  if (props.showValue !== void 0) opts.showValue = props.showValue;
@@ -1345,6 +1520,10 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
1345
1520
  opts.catAxisLabelRotate = props.catAxisLabelRotate;
1346
1521
  if (props.catAxisLabelFontSize !== void 0)
1347
1522
  opts.catAxisLabelFontSize = props.catAxisLabelFontSize;
1523
+ if (props.catAxisLabelFontFace !== void 0)
1524
+ opts.catAxisLabelFontFace = props.catAxisLabelFontFace;
1525
+ if (props.catGridLine !== void 0)
1526
+ opts.catGridLine = resolveGridLine(props.catGridLine, theme, warnings);
1348
1527
  if (props.valAxisTitle !== void 0) {
1349
1528
  opts.valAxisTitle = props.valAxisTitle;
1350
1529
  opts.showValAxisTitle = true;
@@ -1359,14 +1538,22 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
1359
1538
  opts.valAxisLabelFormatCode = props.valAxisLabelFormatCode;
1360
1539
  if (props.valAxisMajorUnit !== void 0)
1361
1540
  opts.valAxisMajorUnit = props.valAxisMajorUnit;
1541
+ if (props.valAxisLabelFontFace !== void 0)
1542
+ opts.valAxisLabelFontFace = props.valAxisLabelFontFace;
1543
+ if (props.valGridLine !== void 0)
1544
+ opts.valGridLine = resolveGridLine(props.valGridLine, theme, warnings);
1362
1545
  if (props.barDir !== void 0) opts.barDir = props.barDir;
1363
1546
  if (props.barGrouping !== void 0) opts.barGrouping = props.barGrouping;
1364
1547
  if (props.barGapWidthPct !== void 0)
1365
1548
  opts.barGapWidthPct = props.barGapWidthPct;
1549
+ if (props.barOverlapPct !== void 0)
1550
+ opts.barOverlapPct = props.barOverlapPct;
1366
1551
  if (props.lineSmooth !== void 0) opts.lineSmooth = props.lineSmooth;
1367
1552
  if (props.lineDataSymbol !== void 0)
1368
1553
  opts.lineDataSymbol = props.lineDataSymbol;
1369
1554
  if (props.lineSize !== void 0) opts.lineSize = props.lineSize;
1555
+ if (props.lineDataSymbolSize !== void 0)
1556
+ opts.lineDataSymbolSize = props.lineDataSymbolSize;
1370
1557
  if (props.firstSliceAng !== void 0)
1371
1558
  opts.firstSliceAng = props.firstSliceAng;
1372
1559
  if (props.holeSize !== void 0) opts.holeSize = props.holeSize;
@@ -1403,7 +1590,7 @@ async function renderComponent(slide, component, theme, pptx, warnings, ctx) {
1403
1590
  );
1404
1591
  break;
1405
1592
  case "shape":
1406
- renderShapeComponent(slide, p, theme, pptx, warnings);
1593
+ renderShapeComponent(slide, p, theme, pptx, warnings, ctx);
1407
1594
  break;
1408
1595
  case "table":
1409
1596
  renderTableComponent(slide, p, theme, pptx, warnings);
@@ -1460,7 +1647,7 @@ function buildSlideTemplateProps(def, theme, warnings) {
1460
1647
 
1461
1648
  // src/core/render.ts
1462
1649
  import { mergeWithDefaults as mergeWithDefaults3 } from "@json-to-office/shared";
1463
- async function renderPresentation(processed, warnings) {
1650
+ async function renderPresentation(processed, warnings, pendingFills) {
1464
1651
  const pptx = new PptxGenJS();
1465
1652
  if (processed.metadata.title) pptx.title = processed.metadata.title;
1466
1653
  if (processed.metadata.author) pptx.author = processed.metadata.author;
@@ -1505,10 +1692,37 @@ async function renderPresentation(processed, warnings) {
1505
1692
  slideCtx,
1506
1693
  services: processed.services,
1507
1694
  slideWidth: processed.slideWidth,
1508
- slideHeight: processed.slideHeight
1695
+ slideHeight: processed.slideHeight,
1696
+ pendingFills
1509
1697
  };
1510
1698
  const slide = slideData.template ? pptx.addSlide({ masterName: slideData.template }) : pptx.addSlide();
1511
- if (slideData.background) {
1699
+ const templateDef = slideData.template ? templateMap.get(slideData.template) : void 0;
1700
+ if (slideData.template && !templateDef) {
1701
+ warn(
1702
+ warnings,
1703
+ W.MISSING_TEMPLATE,
1704
+ `Unknown template "${slideData.template}". Available: ${[...templateMap.keys()].join(", ")}`,
1705
+ { slide: slideIdx }
1706
+ );
1707
+ }
1708
+ const backgroundGradient = slideData.background?.gradient ?? (slideData.background ? void 0 : templateDef?.background?.gradient);
1709
+ if (backgroundGradient) {
1710
+ renderShapeComponent(
1711
+ slide,
1712
+ {
1713
+ type: "rect",
1714
+ x: 0,
1715
+ y: 0,
1716
+ w: processed.slideWidth,
1717
+ h: processed.slideHeight,
1718
+ fill: { gradient: backgroundGradient }
1719
+ },
1720
+ processed.theme,
1721
+ pptx,
1722
+ warnings,
1723
+ renderCtx
1724
+ );
1725
+ } else if (slideData.background) {
1512
1726
  if (slideData.background.color) {
1513
1727
  slide.background = {
1514
1728
  color: resolveColor(
@@ -1528,15 +1742,6 @@ async function renderPresentation(processed, warnings) {
1528
1742
  if (slideData.hidden) {
1529
1743
  slide.hidden = true;
1530
1744
  }
1531
- const templateDef = slideData.template ? templateMap.get(slideData.template) : void 0;
1532
- if (slideData.template && !templateDef) {
1533
- warn(
1534
- warnings,
1535
- W.MISSING_TEMPLATE,
1536
- `Unknown template "${slideData.template}". Available: ${[...templateMap.keys()].join(", ")}`,
1537
- { slide: slideIdx }
1538
- );
1539
- }
1540
1745
  const effectiveGrid = mergeGridConfigs(processed.grid, templateDef?.grid);
1541
1746
  if (templateDef?.objects) {
1542
1747
  for (const obj of templateDef.objects) {
@@ -1708,10 +1913,43 @@ async function resolveDocumentFonts(document, theme, warnings, fonts) {
1708
1913
  return resolved;
1709
1914
  }
1710
1915
 
1711
- // src/core/generator.ts
1916
+ // src/core/generationContext.ts
1712
1917
  import { applyExportMode, scopedThemeName } from "@json-to-office/shared";
1918
+ function resolveThemeContext(documentIn, options = {}) {
1919
+ const { customThemes, fonts, warnings, defaultThemeName, resolveNamedTheme } = options;
1920
+ if (documentIn.props === null) {
1921
+ throw new Error(
1922
+ "Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
1923
+ );
1924
+ }
1925
+ let document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
1926
+ let inlineTheme;
1927
+ if (typeof document.props.theme === "object" && document.props.theme !== null) {
1928
+ inlineTheme = document.props.theme;
1929
+ }
1930
+ const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : document.props.theme ?? defaultThemeName ?? "default";
1931
+ let theme = inlineTheme ?? (resolveNamedTheme ? resolveNamedTheme(baseThemeName) : customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName));
1932
+ const mode = applyExportMode({ doc: document, theme, fonts });
1933
+ document = mode.doc;
1934
+ theme = mode.theme;
1935
+ for (const w of mode.warnings) {
1936
+ warnings?.push({
1937
+ code: w.code,
1938
+ message: w.message,
1939
+ component: "fontRegistry"
1940
+ });
1941
+ }
1942
+ return {
1943
+ document,
1944
+ theme,
1945
+ themeName: scopedThemeName(baseThemeName, fonts?.mode)
1946
+ };
1947
+ }
1948
+
1949
+ // src/core/generator.ts
1713
1950
  import {
1714
1951
  collectImageSourceConflicts,
1952
+ collectTextContentConflicts,
1715
1953
  validateJsonPresentationDocument,
1716
1954
  validatePresentationDocument
1717
1955
  } from "@json-to-office/shared-pptx";
@@ -1721,6 +1959,23 @@ import JSZip from "jszip";
1721
1959
  var MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1722
1960
  var NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1723
1961
  var DEFAULT_GENERATED_AT = "2000-01-01T00:00:00.000Z";
1962
+ function applyPendingFills(xml, pendingFills) {
1963
+ let out = xml;
1964
+ for (const [index, fill] of pendingFills.entries()) {
1965
+ const marker = `name="${fill.objectName}"`;
1966
+ const markerIdx = out.indexOf(marker);
1967
+ if (markerIdx === -1) continue;
1968
+ const spEnd = out.indexOf("</p:sp>", markerIdx);
1969
+ const solidStart = out.indexOf("<a:solidFill>", markerIdx);
1970
+ const solidEndTag = "</a:solidFill>";
1971
+ const solidEnd = out.indexOf(solidEndTag, solidStart);
1972
+ if (solidStart !== -1 && solidEnd !== -1 && spEnd !== -1 && solidStart < spEnd) {
1973
+ out = out.slice(0, solidStart) + fill.xml + out.slice(solidEnd + solidEndTag.length);
1974
+ }
1975
+ out = out.slice(0, markerIdx) + `name="Fill ${index + 1}"` + out.slice(markerIdx + marker.length);
1976
+ }
1977
+ return out;
1978
+ }
1724
1979
  function resolveGeneratedAt(value) {
1725
1980
  const date = value === void 0 ? new Date(DEFAULT_GENERATED_AT) : new Date(value);
1726
1981
  if (Number.isNaN(date.getTime())) {
@@ -1820,9 +2075,21 @@ async function packagePresentationBuffer(buffer, options = {}) {
1820
2075
  let changed = false;
1821
2076
  for (const [path2, entry] of Object.entries(zip.files)) {
1822
2077
  if (!path2.match(/^ppt\/slides\/slide\d+\.xml$/)) continue;
1823
- const xml = await entry.async("string");
2078
+ let xml = await entry.async("string");
2079
+ let fileChanged = false;
1824
2080
  if (xml.includes(MEDIUM_STYLE_2_ACCENT_1)) {
1825
- zip.file(path2, xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID));
2081
+ xml = xml.replaceAll(MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID);
2082
+ fileChanged = true;
2083
+ }
2084
+ if (options.pendingFills?.length) {
2085
+ const withFills = applyPendingFills(xml, options.pendingFills);
2086
+ if (withFills !== xml) {
2087
+ xml = withFills;
2088
+ fileChanged = true;
2089
+ }
2090
+ }
2091
+ if (fileChanged) {
2092
+ zip.file(path2, xml);
1826
2093
  changed = true;
1827
2094
  }
1828
2095
  }
@@ -1858,25 +2125,31 @@ function assertValidPresentation(input, validation) {
1858
2125
  throw new PresentationValidationError(result.errors);
1859
2126
  }
1860
2127
  }
2128
+ function assertNoContentConflicts(document) {
2129
+ const sourceConflicts = [
2130
+ ...collectImageSourceConflicts(document),
2131
+ ...collectTextContentConflicts(document)
2132
+ ];
2133
+ if (sourceConflicts.length > 0) {
2134
+ throw new Error(
2135
+ `Document validation failed:
2136
+ ${sourceConflicts.map((e) => ` - ${e.path}: ${e.message}`).join("\n")}`
2137
+ );
2138
+ }
2139
+ }
1861
2140
  function isPresentationComponentDefinition(definition) {
1862
2141
  if (typeof definition !== "object" || definition === null) return false;
1863
2142
  const def = definition;
1864
2143
  return def.name === "pptx" && "props" in def;
1865
2144
  }
1866
- async function generatePresentation(document, options, warnings) {
2145
+ async function generatePresentation(document, options, warnings, pendingFills) {
1867
2146
  assertValidPresentation(document, options?.validation);
1868
2147
  if (!document || document.name !== "pptx") {
1869
2148
  throw new Error("Top-level component must be a pptx component");
1870
2149
  }
1871
- const sourceConflicts = collectImageSourceConflicts(document);
1872
- if (sourceConflicts.length > 0) {
1873
- throw new Error(
1874
- `Document validation failed:
1875
- ${sourceConflicts.map((e) => ` - ${e.path}: ${e.message}`).join("\n")}`
1876
- );
1877
- }
2150
+ assertNoContentConflicts(document);
1878
2151
  const processed = processPresentation(document, options);
1879
- return await renderPresentation(processed, warnings);
2152
+ return await renderPresentation(processed, warnings, pendingFills);
1880
2153
  }
1881
2154
  async function generateBufferFromJson(jsonConfig, options) {
1882
2155
  const result = await generateBufferWithWarnings(jsonConfig, options);
@@ -1895,64 +2168,34 @@ async function generateBufferWithWarnings(jsonConfig, options) {
1895
2168
  component = jsonConfig;
1896
2169
  }
1897
2170
  const warnings = [];
1898
- if (typeof component.props?.theme === "object" && component.props.theme !== null) {
1899
- const inlineTheme = component.props.theme;
1900
- const inlineName = inlineTheme.name || "inline-theme";
1901
- component = {
1902
- ...component,
1903
- props: { ...component.props, theme: inlineName }
1904
- };
1905
- options = {
1906
- ...options,
1907
- customThemes: {
1908
- ...options?.customThemes ?? {},
1909
- [inlineName]: inlineTheme
1910
- }
1911
- };
1912
- }
1913
- const baseThemeName = component.props?.theme ?? "default";
1914
- let resolvedTheme = options?.customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName);
1915
- const mode = applyExportMode({
1916
- doc: component,
1917
- theme: resolvedTheme,
1918
- fonts: options?.fonts
2171
+ const context = resolveThemeContext(component, {
2172
+ customThemes: options?.customThemes,
2173
+ fonts: options?.fonts,
2174
+ warnings
1919
2175
  });
1920
- component = mode.doc;
1921
- resolvedTheme = mode.theme;
1922
- for (const w of mode.warnings) {
1923
- warnings.push({
1924
- code: w.code,
1925
- message: w.message,
1926
- component: "fontRegistry"
1927
- });
1928
- }
2176
+ component = context.document;
1929
2177
  await resolveDocumentFonts(
1930
2178
  component,
1931
- resolvedTheme,
2179
+ context.theme,
1932
2180
  warnings,
1933
2181
  options?.fonts
1934
2182
  );
1935
- const themeName = scopedThemeName(baseThemeName, options?.fonts?.mode);
1936
- if (themeName !== baseThemeName) {
1937
- component = {
1938
- ...component,
1939
- props: { ...component.props, theme: themeName }
1940
- };
1941
- }
1942
2183
  const effectiveOptions = {
1943
2184
  ...options,
1944
- customThemes: {
1945
- ...options?.customThemes ?? {},
1946
- [themeName]: resolvedTheme
1947
- }
2185
+ theme: context.theme
1948
2186
  };
2187
+ const pendingFills = [];
1949
2188
  const pptx = await generatePresentation(
1950
2189
  component,
1951
2190
  effectiveOptions,
1952
- warnings
2191
+ warnings,
2192
+ pendingFills
1953
2193
  );
1954
2194
  const data = await pptx.write({ outputType: "nodebuffer" });
1955
- const buffer = await packagePresentationBuffer(data, options);
2195
+ const buffer = await packagePresentationBuffer(data, {
2196
+ ...options,
2197
+ pendingFills
2198
+ });
1956
2199
  return { buffer, warnings };
1957
2200
  }
1958
2201
  async function generateAndSaveFromJson(jsonConfig, outputPath, options) {
@@ -2107,7 +2350,6 @@ async function exportPluginSchema(customComponents, outputPath, options = {}) {
2107
2350
  }
2108
2351
 
2109
2352
  // src/plugin/createPresentationGenerator.ts
2110
- import { applyExportMode as applyExportMode2, scopedThemeName as scopedThemeName2 } from "@json-to-office/shared";
2111
2353
  function createBuilderImpl(state) {
2112
2354
  const componentMap = new Map(state.components.map((c) => [c.name, c]));
2113
2355
  async function processSlideComponents(components, warningsCollector, theme, validateEmitted, parentName, depth = 0) {
@@ -2251,41 +2493,23 @@ function createBuilderImpl(state) {
2251
2493
  } else if (!internalDocument || internalDocument.name !== "pptx") {
2252
2494
  throw new Error("Top-level component must be a pptx component");
2253
2495
  }
2254
- let inlineTheme;
2255
- if (typeof internalDocument.props.theme === "object" && internalDocument.props.theme !== null) {
2256
- inlineTheme = internalDocument.props.theme;
2257
- internalDocument = {
2258
- ...internalDocument,
2259
- props: {
2260
- ...internalDocument.props,
2261
- theme: inlineTheme.name || "inline-theme"
2262
- }
2263
- };
2264
- }
2265
- const docThemeName = internalDocument.props.theme;
2266
- const baseThemeName = docThemeName ?? (typeof state.theme === "string" ? state.theme : "default");
2267
- let resolvedTheme = inlineTheme ?? state.customThemes?.[baseThemeName] ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(baseThemeName));
2268
2496
  const warnings = [];
2269
- const mode = applyExportMode2({
2270
- doc: internalDocument,
2271
- theme: resolvedTheme,
2272
- fonts: state.fonts
2497
+ const context = resolveThemeContext(internalDocument, {
2498
+ customThemes: state.customThemes,
2499
+ fonts: state.fonts,
2500
+ warnings,
2501
+ defaultThemeName: typeof state.theme === "string" ? state.theme : void 0,
2502
+ resolveNamedTheme: (name) => state.customThemes?.[name] ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(name))
2273
2503
  });
2274
- resolvedTheme = mode.theme;
2275
- for (const w of mode.warnings) {
2276
- warnings.push({
2277
- code: w.code,
2278
- message: w.message,
2279
- component: "fontRegistry"
2280
- });
2281
- }
2504
+ const modedRoot = context.document;
2505
+ const resolvedTheme = context.theme;
2282
2506
  const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
2283
2507
  let validationDocument;
2284
2508
  if (parentName === "pptx") {
2285
- validationDocument = { ...mode.doc, children: emitted };
2509
+ validationDocument = { ...modedRoot, children: emitted };
2286
2510
  } else if (parentName === "slide") {
2287
2511
  validationDocument = {
2288
- ...mode.doc,
2512
+ ...modedRoot,
2289
2513
  children: [{ name: "slide", props: {}, children: emitted }]
2290
2514
  };
2291
2515
  } else {
@@ -2306,19 +2530,16 @@ function createBuilderImpl(state) {
2306
2530
  );
2307
2531
  }
2308
2532
  };
2309
- const processedChildren = mode.doc.children ? await processAllSlides(
2310
- mode.doc.children,
2533
+ const processedChildren = modedRoot.children ? await processAllSlides(
2534
+ modedRoot.children,
2311
2535
  warnings,
2312
2536
  resolvedTheme,
2313
2537
  validateEmitted
2314
2538
  ) : [];
2315
- const themeName = scopedThemeName2(baseThemeName, state.fonts?.mode);
2316
- const docWithScopedTheme = themeName !== baseThemeName ? {
2317
- ...mode.doc,
2318
- props: { ...mode.doc.props, theme: themeName },
2539
+ const processedDocument = {
2540
+ ...modedRoot,
2319
2541
  children: processedChildren
2320
- } : { ...mode.doc, children: processedChildren };
2321
- const processedDocument = docWithScopedTheme;
2542
+ };
2322
2543
  if (validationOptions.enabled !== false) {
2323
2544
  const result = validatePresentation(processedDocument, [], {
2324
2545
  allowUnknownFields: validationOptions.allowUnknownFields
@@ -2339,19 +2560,18 @@ function createBuilderImpl(state) {
2339
2560
  warnings,
2340
2561
  state.fonts
2341
2562
  );
2342
- const effectiveCustomThemes = {
2343
- ...state.customThemes ?? {},
2344
- [themeName]: resolvedTheme
2345
- };
2563
+ assertNoContentConflicts(processedDocument);
2346
2564
  const processed = processPresentation(processedDocument, {
2347
- customThemes: effectiveCustomThemes,
2565
+ theme: resolvedTheme,
2348
2566
  services: state.services
2349
2567
  });
2350
- const pptx = await renderPresentation(processed, warnings);
2568
+ const pendingFills = [];
2569
+ const pptx = await renderPresentation(processed, warnings, pendingFills);
2351
2570
  const data = await pptx.write({ outputType: "nodebuffer" });
2352
2571
  const buffer = await packagePresentationBuffer(data, {
2353
2572
  deterministic: options?.deterministic ?? state.packaging.deterministic,
2354
- generatedAt: options?.generatedAt ?? state.packaging.generatedAt
2573
+ generatedAt: options?.generatedAt ?? state.packaging.generatedAt,
2574
+ pendingFills
2355
2575
  });
2356
2576
  return { buffer, warnings };
2357
2577
  } catch (error) {