@markdstage/markdstage 3.0.0 → 3.2.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.
@@ -18,6 +18,7 @@ const NS_REL =
18
18
  "http://schemas.openxmlformats.org/package/2006/relationships";
19
19
  const JAPANESE_FONT_FACE = "Yu Gothic";
20
20
  const HUNDREDTH_POINTS_PER_PIXEL = 75;
21
+ const SLIDE_LAYOUT_ID_BASE = 2147500000;
21
22
 
22
23
  const REL = {
23
24
  officeDocument: `${NS_R}/officeDocument`,
@@ -195,13 +196,18 @@ function colorOf(value, path) {
195
196
  };
196
197
  }
197
198
 
198
- function colorXml(value, path, opacity = 1) {
199
+ function colorChoiceXml(value, path, opacity = 1) {
199
200
  const color = colorOf(value, path);
200
- if (!color) return "<a:noFill/>";
201
+ if (!color) return null;
201
202
  const alpha = Math.round(color.alpha * opacity * 100000);
202
- return `<a:solidFill><a:srgbClr val="${color.hex}">${
203
+ return `<a:srgbClr val="${color.hex}">${
203
204
  alpha < 100000 ? `<a:alpha val="${alpha}"/>` : ""
204
- }</a:srgbClr></a:solidFill>`;
205
+ }</a:srgbClr>`;
206
+ }
207
+
208
+ function colorXml(value, path, opacity = 1) {
209
+ const color = colorChoiceXml(value, path, opacity);
210
+ return color ? `<a:solidFill>${color}</a:solidFill>` : "<a:noFill/>";
205
211
  }
206
212
 
207
213
  function lineXml(element, path) {
@@ -369,6 +375,23 @@ function bulletTextOffsetPx(paragraph, path) {
369
375
  return largestRunSize / HUNDREDTH_POINTS_PER_PIXEL;
370
376
  }
371
377
 
378
+ function paragraphSpacingXml(paragraph, path) {
379
+ const entries = [
380
+ ["lineSpacing", "a:lnSpc", true],
381
+ ["spaceBefore", "a:spcBef", false],
382
+ ["spaceAfter", "a:spcAft", false],
383
+ ];
384
+ return entries
385
+ .map(([property, tag, positive]) => {
386
+ if (paragraph[property] === undefined) return "";
387
+ const value = positive
388
+ ? positiveNumber(paragraph[property], `${path}.${property}`)
389
+ : nonNegativeNumber(paragraph[property], `${path}.${property}`);
390
+ return `<${tag}><a:spcPts val="${Math.round(value * HUNDREDTH_POINTS_PER_PIXEL)}"/></${tag}>`;
391
+ })
392
+ .join("");
393
+ }
394
+
372
395
  function paragraphXml(paragraph, path, relationships, leftMarginPx = 0) {
373
396
  if (!paragraph || typeof paragraph !== "object" || Array.isArray(paragraph)) {
374
397
  fail(`${path} must be an object`);
@@ -389,14 +412,29 @@ function paragraphXml(paragraph, path, relationships, leftMarginPx = 0) {
389
412
  }
390
413
  let bullet = "";
391
414
  if (paragraph.bullet) {
415
+ const bulletValue =
416
+ typeof paragraph.bullet === "string" ? null : paragraph.bullet;
392
417
  const character =
393
- typeof paragraph.bullet === "string"
418
+ !bulletValue
394
419
  ? paragraph.bullet
395
- : paragraph.bullet.character || "•";
396
- bullet = `<a:buChar char="${xmlEscape(character)}"/>`;
420
+ : bulletValue.character || "•";
421
+ const bulletColor =
422
+ bulletValue?.color === undefined
423
+ ? null
424
+ : colorChoiceXml(bulletValue.color, `${path}.bullet.color`);
425
+ bullet = `${
426
+ bulletColor ? `<a:buClr>${bulletColor}</a:buClr>` : ""
427
+ }<a:buChar char="${xmlEscape(character)}"/>`;
397
428
  }
398
429
  const bulletOffsetPx = bulletTextOffsetPx(paragraph, path);
399
- const paragraphMarginPx = Math.max(leftMarginPx, bulletOffsetPx);
430
+ const leftMargin =
431
+ paragraph.leftMargin === undefined
432
+ ? 0
433
+ : nonNegativeNumber(paragraph.leftMargin, `${path}.leftMargin`);
434
+ const paragraphMarginPx = Math.max(
435
+ leftMarginPx + leftMargin,
436
+ bulletOffsetPx,
437
+ );
400
438
  const indentation = [
401
439
  paragraphMarginPx > 0 ? `marL="${emu(paragraphMarginPx)}"` : "",
402
440
  bulletOffsetPx > 0 ? `indent="-${emu(bulletOffsetPx)}"` : "",
@@ -404,12 +442,13 @@ function paragraphXml(paragraph, path, relationships, leftMarginPx = 0) {
404
442
  .filter(Boolean)
405
443
  .join(" ");
406
444
  const indentationAttributes = indentation ? ` ${indentation}` : "";
445
+ const spacing = paragraphSpacingXml(paragraph, path);
407
446
  const runs = paragraph.runs
408
447
  .map((run, index) =>
409
448
  runXml(run, `${path}.runs[${index}]`, relationships),
410
449
  )
411
450
  .join("");
412
- return `<a:p><a:pPr algn="${align}" lvl="${level}"${indentationAttributes}>${bullet}</a:pPr>${runs}<a:endParaRPr lang="en-US" noProof="1"><a:ea typeface="${JAPANESE_FONT_FACE}"/></a:endParaRPr></a:p>`;
451
+ return `<a:p><a:pPr algn="${align}" lvl="${level}"${indentationAttributes}>${spacing}${bullet}</a:pPr>${runs}<a:endParaRPr lang="en-US" noProof="1"><a:ea typeface="${JAPANESE_FONT_FACE}"/></a:endParaRPr></a:p>`;
413
452
  }
414
453
 
415
454
  function runXml(run, path, relationships) {
@@ -600,8 +639,17 @@ function nativeShapeXml(element, path, id, relationships) {
600
639
  return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="Shape ${id}"/><p:cNvSpPr txBox="0"/><p:nvPr/></p:nvSpPr><p:spPr>${properties}</p:spPr>${text}</p:sp>`;
601
640
  }
602
641
 
603
- function pictureXml(asset, bounds, id, relationshipId, name = `Image ${id}`) {
604
- return `<p:pic><p:nvPicPr><p:cNvPr id="${id}" name="${xmlEscape(name)}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${relationshipId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr>${xfrmXml(bounds)}<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
642
+ function pictureXml(
643
+ asset,
644
+ bounds,
645
+ id,
646
+ relationshipId,
647
+ name = `Image ${id}`,
648
+ userDrawn = false,
649
+ shape = "rect",
650
+ ) {
651
+ const preset = shape === "roundedRect" ? "roundRect" : "rect";
652
+ return `<p:pic><p:nvPicPr><p:cNvPr id="${id}" name="${xmlEscape(name)}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr${userDrawn ? ' userDrawn="1"' : ""}/></p:nvPicPr><p:blipFill><a:blip r:embed="${relationshipId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr>${xfrmXml(bounds)}<a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
605
653
  }
606
654
 
607
655
  function tableXml(element, path, id, relationships) {
@@ -747,12 +795,12 @@ function connectorXml(element, path, nextId, relationships) {
747
795
  return shapes.join("");
748
796
  }
749
797
 
750
- function relationshipRegistry(notesSlideNumber = null) {
798
+ function relationshipRegistry(layoutNumber, notesSlideNumber = null) {
751
799
  const entries = [
752
800
  {
753
801
  id: "rId1",
754
802
  type: REL.slideLayout,
755
- target: "../slideLayouts/slideLayout1.xml",
803
+ target: `../slideLayouts/slideLayout${layoutNumber}.xml`,
756
804
  external: false,
757
805
  },
758
806
  ];
@@ -797,24 +845,33 @@ function baseShapeTree() {
797
845
  return '<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>';
798
846
  }
799
847
 
800
- function buildSlide(slide, slideIndex, assets, notesSlideNumber = null) {
848
+ function buildSlide(
849
+ slide,
850
+ slideIndex,
851
+ assets,
852
+ layoutNumber,
853
+ notesSlideNumber = null,
854
+ ) {
801
855
  const path = `slides[${slideIndex}]`;
802
856
  if (!slide || typeof slide !== "object" || Array.isArray(slide)) {
803
857
  fail(`${path} must be an object`);
804
858
  }
805
859
  if (!Array.isArray(slide.elements)) fail(`${path}.elements must be an array`);
806
- const relationships = relationshipRegistry(notesSlideNumber);
860
+ const relationships = relationshipRegistry(layoutNumber, notesSlideNumber);
807
861
  let shapeId = 2;
808
862
  const nextId = () => shapeId++;
809
863
  const shapes = [];
810
- if (slide.backgroundAssetId !== undefined) {
864
+ const artworkAssetId = slide.artworkAssetId ?? slide.backgroundAssetId;
865
+ if (artworkAssetId !== undefined) {
811
866
  const asset = requireAsset(
812
867
  assets,
813
- slide.backgroundAssetId,
814
- `${path}.backgroundAssetId`,
868
+ artworkAssetId,
869
+ slide.artworkAssetId !== undefined
870
+ ? `${path}.artworkAssetId`
871
+ : `${path}.backgroundAssetId`,
815
872
  );
816
873
  if (asset.contentType !== "image/png") {
817
- fail(`${path}.backgroundAssetId must reference a PNG asset`);
874
+ fail(`${path}.artworkAssetId must reference a PNG asset`);
818
875
  }
819
876
  const id = nextId();
820
877
  shapes.push(
@@ -828,7 +885,7 @@ function buildSlide(slide, slideIndex, assets, notesSlideNumber = null) {
828
885
  },
829
886
  id,
830
887
  relationships.image(asset),
831
- "Slide background",
888
+ slide.artworkAssetId !== undefined ? "Slide artwork" : "Slide background",
832
889
  ),
833
890
  );
834
891
  }
@@ -850,6 +907,9 @@ function buildSlide(slide, slideIndex, assets, notesSlideNumber = null) {
850
907
  boundsOf(element, elementPath),
851
908
  nextId(),
852
909
  relationships.image(asset),
910
+ element.name || element.alt || undefined,
911
+ false,
912
+ element.shape,
853
913
  ),
854
914
  );
855
915
  } else if (element.type === "shape") {
@@ -877,7 +937,13 @@ function relationshipsXml(entries) {
877
937
  .join("")}</Relationships>`;
878
938
  }
879
939
 
880
- function contentTypesXml(slideCount, assets, notesCount) {
940
+ function contentTypesXml(
941
+ slideCount,
942
+ assets,
943
+ notesCount,
944
+ masterCount,
945
+ layoutCount,
946
+ ) {
881
947
  const imageTypes = new Map();
882
948
  for (const asset of assets.values()) {
883
949
  imageTypes.set(asset.extension, asset.contentType);
@@ -904,40 +970,55 @@ function contentTypesXml(slideCount, assets, notesCount) {
904
970
  : "";
905
971
  const notesTheme =
906
972
  notesCount > 0
907
- ? '<Override PartName="/ppt/theme/theme2.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>'
973
+ ? `<Override PartName="/ppt/theme/theme${masterCount + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>`
908
974
  : "";
909
- return `${XML}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${defaults}<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/><Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>${notesTheme}${slides}${notesMaster}${notes}</Types>`;
975
+ const masters = Array.from(
976
+ { length: masterCount },
977
+ (_, index) =>
978
+ `<Override PartName="/ppt/slideMasters/slideMaster${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/><Override PartName="/ppt/theme/theme${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>`,
979
+ ).join("");
980
+ const layouts = Array.from(
981
+ { length: layoutCount },
982
+ (_, index) =>
983
+ `<Override PartName="/ppt/slideLayouts/slideLayout${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>`,
984
+ ).join("");
985
+ return `${XML}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${defaults}<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>${masters}${layouts}${notesTheme}${slides}${notesMaster}${notes}</Types>`;
910
986
  }
911
987
 
912
- function presentationXml(slideCount, notesCount) {
988
+ function presentationXml(masterCount, slideCount, notesCount) {
989
+ const masters = Array.from(
990
+ { length: masterCount },
991
+ (_, index) =>
992
+ `<p:sldMasterId id="${2147483648 + index}" r:id="rId${index + 1}"/>`,
993
+ ).join("");
913
994
  const slides = Array.from(
914
995
  { length: slideCount },
915
996
  (_, index) =>
916
- `<p:sldId id="${256 + index}" r:id="rId${index + 2}"/>`,
997
+ `<p:sldId id="${256 + index}" r:id="rId${masterCount + index + 1}"/>`,
917
998
  ).join("");
918
999
  const notesMaster =
919
1000
  notesCount > 0
920
- ? `<p:notesMasterIdLst><p:notesMasterId r:id="rId${slideCount + 2}"/></p:notesMasterIdLst>`
1001
+ ? `<p:notesMasterIdLst><p:notesMasterId r:id="rId${masterCount + slideCount + 1}"/></p:notesMasterIdLst>`
921
1002
  : "";
922
- return `${XML}<p:presentation xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>${notesMaster}<p:sldIdLst>${slides}</p:sldIdLst><p:sldSz cx="${PPTX_DIMENSIONS.widthEmu}" cy="${PPTX_DIMENSIONS.heightEmu}" type="screen16x9"/><p:notesSz cx="6858000" cy="9144000"/><p:defaultTextStyle/></p:presentation>`;
1003
+ return `${XML}<p:presentation xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:sldMasterIdLst>${masters}</p:sldMasterIdLst>${notesMaster}<p:sldIdLst>${slides}</p:sldIdLst><p:sldSz cx="${PPTX_DIMENSIONS.widthEmu}" cy="${PPTX_DIMENSIONS.heightEmu}" type="screen16x9"/><p:notesSz cx="6858000" cy="9144000"/><p:defaultTextStyle/></p:presentation>`;
923
1004
  }
924
1005
 
925
- function presentationRelsXml(slideCount, notesCount) {
1006
+ function presentationRelsXml(masterCount, slideCount, notesCount) {
926
1007
  const entries = [
927
- {
928
- id: "rId1",
1008
+ ...Array.from({ length: masterCount }, (_, index) => ({
1009
+ id: `rId${index + 1}`,
929
1010
  type: REL.slideMaster,
930
- target: "slideMasters/slideMaster1.xml",
931
- },
1011
+ target: `slideMasters/slideMaster${index + 1}.xml`,
1012
+ })),
932
1013
  ...Array.from({ length: slideCount }, (_, index) => ({
933
- id: `rId${index + 2}`,
1014
+ id: `rId${masterCount + index + 1}`,
934
1015
  type: REL.slide,
935
1016
  target: `slides/slide${index + 1}.xml`,
936
1017
  })),
937
1018
  ];
938
1019
  if (notesCount > 0) {
939
1020
  entries.push({
940
- id: `rId${slideCount + 2}`,
1021
+ id: `rId${masterCount + slideCount + 1}`,
941
1022
  type: REL.notesMaster,
942
1023
  target: "notesMasters/notesMaster1.xml",
943
1024
  });
@@ -953,16 +1034,74 @@ function appXml(slideCount, notesCount) {
953
1034
  return `${XML}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>MarkdStage</Application><PresentationFormat>On-screen Show (16:9)</PresentationFormat><Slides>${slideCount}</Slides><Notes>${notesCount}</Notes><HiddenSlides>0</HiddenSlides><MMClips>0</MMClips><ScaleCrop>false</ScaleCrop><Company/><LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>1.0</AppVersion></Properties>`;
954
1035
  }
955
1036
 
956
- function themeXml() {
957
- return `${XML}<a:theme xmlns:a="${NS_A}" name="MarkdStage"><a:themeElements><a:clrScheme name="MarkdStage"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F1F1F"/></a:dk2><a:lt2><a:srgbClr val="F2F2F2"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="MarkdStage"><a:majorFont><a:latin typeface="Aptos Display"/><a:ea typeface="${JAPANESE_FONT_FACE}"/><a:cs typeface=""/><a:font script="Jpan" typeface="${JAPANESE_FONT_FACE}"/></a:majorFont><a:minorFont><a:latin typeface="Aptos"/><a:ea typeface="${JAPANESE_FONT_FACE}"/><a:cs typeface=""/><a:font script="Jpan" typeface="${JAPANESE_FONT_FACE}"/></a:minorFont></a:fontScheme><a:fmtScheme name="MarkdStage"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="accent1"/></a:solidFill><a:solidFill><a:schemeClr val="accent2"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="28575"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="lt1"/></a:solidFill><a:solidFill><a:schemeClr val="lt2"/></a:solidFill><a:solidFill><a:schemeClr val="dk1"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>`;
1037
+ function themeXml(name = "MarkdStage") {
1038
+ const themeName = xmlEscape(name);
1039
+ return `${XML}<a:theme xmlns:a="${NS_A}" name="${themeName}"><a:themeElements><a:clrScheme name="${themeName}"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F1F1F"/></a:dk2><a:lt2><a:srgbClr val="F2F2F2"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="${themeName}"><a:majorFont><a:latin typeface="Aptos Display"/><a:ea typeface="${JAPANESE_FONT_FACE}"/><a:cs typeface=""/><a:font script="Jpan" typeface="${JAPANESE_FONT_FACE}"/></a:majorFont><a:minorFont><a:latin typeface="Aptos"/><a:ea typeface="${JAPANESE_FONT_FACE}"/><a:cs typeface=""/><a:font script="Jpan" typeface="${JAPANESE_FONT_FACE}"/></a:minorFont></a:fontScheme><a:fmtScheme name="${themeName}"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="accent1"/></a:solidFill><a:solidFill><a:schemeClr val="accent2"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="28575"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="lt1"/></a:solidFill><a:solidFill><a:schemeClr val="lt2"/></a:solidFill><a:solidFill><a:schemeClr val="dk1"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>`;
958
1040
  }
959
1041
 
960
- function slideMasterXml() {
961
- return `${XML}<p:sldMaster xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld name="MarkdStage"><p:spTree>${baseShapeTree()}</p:spTree></p:cSld><p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/><p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst><p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles></p:sldMaster>`;
1042
+ function slideMasterXml(master, layoutNumbers) {
1043
+ const layouts = layoutNumbers
1044
+ .map(
1045
+ (layoutNumber, index) =>
1046
+ `<p:sldLayoutId id="${SLIDE_LAYOUT_ID_BASE + layoutNumber}" r:id="rId${index + 1}"/>`,
1047
+ )
1048
+ .join("");
1049
+ return `${XML}<p:sldMaster xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld name="${xmlEscape(`MarkdStage ${master.theme}`)}"><p:spTree>${baseShapeTree()}</p:spTree></p:cSld><p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/><p:sldLayoutIdLst>${layouts}</p:sldLayoutIdLst><p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles></p:sldMaster>`;
962
1050
  }
963
1051
 
964
- function slideLayoutXml() {
965
- return `${XML}<p:sldLayout xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}" type="blank" preserve="1"><p:cSld name="Blank"><p:spTree>${baseShapeTree()}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`;
1052
+ function buildSlideLayout(layout, layoutIndex, assets) {
1053
+ const path = `layouts[${layoutIndex}]`;
1054
+ const shapes = [];
1055
+ const imageRelationships = [];
1056
+ let shapeId = 2;
1057
+ let relationshipId = 2;
1058
+ const addImage = (asset, bounds, name) => {
1059
+ const id = `rId${relationshipId}`;
1060
+ relationshipId += 1;
1061
+ imageRelationships.push({ id, asset });
1062
+ shapes.push(pictureXml(asset, bounds, shapeId, id, name, true));
1063
+ shapeId += 1;
1064
+ };
1065
+ if (layout.artworkAssetId !== undefined) {
1066
+ const asset = requireAsset(
1067
+ assets,
1068
+ layout.artworkAssetId,
1069
+ `${path}.artworkAssetId`,
1070
+ );
1071
+ if (asset.contentType !== "image/png") {
1072
+ fail(`${path}.artworkAssetId must reference a PNG asset`);
1073
+ }
1074
+ addImage(
1075
+ asset,
1076
+ {
1077
+ x: 0,
1078
+ y: 0,
1079
+ width: PPTX_DIMENSIONS.widthPx,
1080
+ height: PPTX_DIMENSIONS.heightPx,
1081
+ },
1082
+ "Layout artwork",
1083
+ );
1084
+ }
1085
+ const elements = layout.elements ?? [];
1086
+ if (!Array.isArray(elements)) fail(`${path}.elements must be an array`);
1087
+ for (let index = 0; index < elements.length; index += 1) {
1088
+ const element = elements[index];
1089
+ const elementPath = `${path}.elements[${index}]`;
1090
+ if (!element || typeof element !== "object" || Array.isArray(element)) {
1091
+ fail(`${elementPath} must be an object`);
1092
+ }
1093
+ if (element.type !== "image") fail(`${elementPath}.type is not supported`);
1094
+ const asset = requireAsset(assets, element.assetId, `${elementPath}.assetId`);
1095
+ addImage(
1096
+ asset,
1097
+ boundsOf(element, elementPath),
1098
+ element.name || element.alt || `Layout image ${index + 1}`,
1099
+ );
1100
+ }
1101
+ return {
1102
+ xml: `${XML}<p:sldLayout xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}" type="blank" preserve="1" matchingName="${xmlEscape(layout.name)}"><p:cSld name="${xmlEscape(layout.name)}"><p:spTree>${baseShapeTree()}${shapes.join("")}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`,
1103
+ imageRelationships,
1104
+ };
966
1105
  }
967
1106
 
968
1107
  function notesPlaceholderXml({ id, name, type, idx, x, y, cx, cy, paragraphs = "" }) {
@@ -1193,18 +1332,142 @@ function packageEntries(buffer) {
1193
1332
  return files;
1194
1333
  }
1195
1334
 
1196
- export function buildPptxPackage({ title = "Presentation", slides, assets = [] } = {}) {
1335
+ function normalizePresentationStructure(masters, layouts, slides) {
1336
+ let normalizedMasters = masters;
1337
+ let normalizedLayouts = layouts;
1338
+ let normalizedSlides = slides;
1339
+ if (normalizedMasters === undefined && normalizedLayouts === undefined) {
1340
+ normalizedMasters = [
1341
+ {
1342
+ id: "default",
1343
+ theme: "default",
1344
+ layoutIds: ["default"],
1345
+ },
1346
+ ];
1347
+ normalizedLayouts = [
1348
+ {
1349
+ id: "default",
1350
+ name: "default",
1351
+ theme: "default",
1352
+ },
1353
+ ];
1354
+ normalizedSlides = slides.map((slide) => ({
1355
+ ...slide,
1356
+ layoutId: slide.layoutId ?? "default",
1357
+ }));
1358
+ }
1359
+ if (!Array.isArray(normalizedMasters) || normalizedMasters.length === 0) {
1360
+ fail("masters must be a non-empty array");
1361
+ }
1362
+ if (!Array.isArray(normalizedLayouts) || normalizedLayouts.length === 0) {
1363
+ fail("layouts must be a non-empty array");
1364
+ }
1365
+
1366
+ const layoutById = new Map();
1367
+ normalizedLayouts.forEach((layout, index) => {
1368
+ const path = `layouts[${index}]`;
1369
+ if (!layout || typeof layout !== "object" || Array.isArray(layout)) {
1370
+ fail(`${path} must be an object`);
1371
+ }
1372
+ if (typeof layout.id !== "string" || !layout.id) {
1373
+ fail(`${path}.id must be a non-empty string`);
1374
+ }
1375
+ if (layoutById.has(layout.id)) fail(`duplicate layout id "${layout.id}"`);
1376
+ if (typeof layout.name !== "string" || !layout.name) {
1377
+ fail(`${path}.name must be a non-empty string`);
1378
+ }
1379
+ if (typeof layout.theme !== "string" || !layout.theme) {
1380
+ fail(`${path}.theme must be a non-empty string`);
1381
+ }
1382
+ layoutById.set(layout.id, {
1383
+ ...layout,
1384
+ number: index + 1,
1385
+ masterNumber: 0,
1386
+ });
1387
+ });
1388
+
1389
+ const masterIds = new Set();
1390
+ const assignedLayouts = new Set();
1391
+ const preparedMasters = normalizedMasters.map((master, index) => {
1392
+ const path = `masters[${index}]`;
1393
+ if (!master || typeof master !== "object" || Array.isArray(master)) {
1394
+ fail(`${path} must be an object`);
1395
+ }
1396
+ if (typeof master.id !== "string" || !master.id) {
1397
+ fail(`${path}.id must be a non-empty string`);
1398
+ }
1399
+ if (masterIds.has(master.id)) fail(`duplicate master id "${master.id}"`);
1400
+ masterIds.add(master.id);
1401
+ if (typeof master.theme !== "string" || !master.theme) {
1402
+ fail(`${path}.theme must be a non-empty string`);
1403
+ }
1404
+ if (!Array.isArray(master.layoutIds) || master.layoutIds.length === 0) {
1405
+ fail(`${path}.layoutIds must be a non-empty array`);
1406
+ }
1407
+ const layoutNumbers = master.layoutIds.map((layoutId, layoutIndex) => {
1408
+ if (typeof layoutId !== "string" || !layoutId) {
1409
+ fail(`${path}.layoutIds[${layoutIndex}] must be a non-empty string`);
1410
+ }
1411
+ const layout = layoutById.get(layoutId);
1412
+ if (!layout) fail(`${path} references missing layout "${layoutId}"`);
1413
+ if (assignedLayouts.has(layoutId)) {
1414
+ fail(`layout "${layoutId}" belongs to more than one master`);
1415
+ }
1416
+ if (layout.theme !== master.theme) {
1417
+ fail(`layout "${layoutId}" does not match master theme "${master.theme}"`);
1418
+ }
1419
+ assignedLayouts.add(layoutId);
1420
+ layout.masterNumber = index + 1;
1421
+ return layout.number;
1422
+ });
1423
+ return { ...master, number: index + 1, layoutNumbers };
1424
+ });
1425
+ if (assignedLayouts.size !== normalizedLayouts.length) {
1426
+ fail("every layout must belong to exactly one master");
1427
+ }
1428
+
1429
+ const preparedSlides = normalizedSlides.map((slide, index) => {
1430
+ const layoutId = slide?.layoutId;
1431
+ if (typeof layoutId !== "string" || !layoutId) {
1432
+ fail(`slides[${index}].layoutId must be a non-empty string`);
1433
+ }
1434
+ const layout = layoutById.get(layoutId);
1435
+ if (!layout) fail(`slides[${index}] references missing layout "${layoutId}"`);
1436
+ return { slide, layoutNumber: layout.number };
1437
+ });
1438
+
1439
+ return {
1440
+ masters: preparedMasters,
1441
+ layouts: [...layoutById.values()],
1442
+ slides: preparedSlides,
1443
+ };
1444
+ }
1445
+
1446
+ export function buildPptxPackage({
1447
+ title = "Presentation",
1448
+ masters,
1449
+ layouts,
1450
+ slides,
1451
+ assets = [],
1452
+ } = {}) {
1197
1453
  if (typeof title !== "string") fail("title must be a string");
1198
1454
  if (!Array.isArray(slides) || slides.length === 0) {
1199
1455
  fail("slides must be a non-empty array");
1200
1456
  }
1201
1457
  if (slides.length > 0x7ffffeff) fail("slides contains too many items");
1202
1458
  const normalizedAssets = normalizeAssets(assets);
1459
+ const structure = normalizePresentationStructure(masters, layouts, slides);
1203
1460
  const notesSlides = [];
1204
- const builtSlides = slides.map((slide, index) => {
1461
+ const builtSlides = structure.slides.map(({ slide, layoutNumber }, index) => {
1205
1462
  const notes = normalizedNotes(slide, index);
1206
1463
  const notesSlideNumber = notes ? notesSlides.length + 1 : null;
1207
- const built = buildSlide(slide, index, normalizedAssets, notesSlideNumber);
1464
+ const built = buildSlide(
1465
+ slide,
1466
+ index,
1467
+ normalizedAssets,
1468
+ layoutNumber,
1469
+ notesSlideNumber,
1470
+ );
1208
1471
  if (notes) {
1209
1472
  notesSlides.push({
1210
1473
  notes,
@@ -1214,10 +1477,19 @@ export function buildPptxPackage({ title = "Presentation", slides, assets = [] }
1214
1477
  }
1215
1478
  return built;
1216
1479
  });
1480
+ const builtLayouts = structure.layouts.map((layout, index) =>
1481
+ buildSlideLayout(layout, index, normalizedAssets),
1482
+ );
1217
1483
  const entries = [
1218
1484
  {
1219
1485
  name: "[Content_Types].xml",
1220
- data: contentTypesXml(slides.length, normalizedAssets, notesSlides.length),
1486
+ data: contentTypesXml(
1487
+ slides.length,
1488
+ normalizedAssets,
1489
+ notesSlides.length,
1490
+ structure.masters.length,
1491
+ structure.layouts.length,
1492
+ ),
1221
1493
  },
1222
1494
  {
1223
1495
  name: "_rels/.rels",
@@ -1231,44 +1503,85 @@ export function buildPptxPackage({ title = "Presentation", slides, assets = [] }
1231
1503
  { name: "docProps/app.xml", data: appXml(slides.length, notesSlides.length) },
1232
1504
  {
1233
1505
  name: "ppt/presentation.xml",
1234
- data: presentationXml(slides.length, notesSlides.length),
1506
+ data: presentationXml(
1507
+ structure.masters.length,
1508
+ slides.length,
1509
+ notesSlides.length,
1510
+ ),
1235
1511
  },
1236
1512
  {
1237
1513
  name: "ppt/_rels/presentation.xml.rels",
1238
- data: presentationRelsXml(slides.length, notesSlides.length),
1514
+ data: presentationRelsXml(
1515
+ structure.masters.length,
1516
+ slides.length,
1517
+ notesSlides.length,
1518
+ ),
1239
1519
  },
1240
- { name: "ppt/theme/theme1.xml", data: themeXml() },
1241
- { name: "ppt/slideMasters/slideMaster1.xml", data: slideMasterXml() },
1242
- {
1243
- name: "ppt/slideMasters/_rels/slideMaster1.xml.rels",
1244
- data: relationshipsXml([
1520
+ ...structure.masters.flatMap((master) => [
1521
+ {
1522
+ name: `ppt/theme/theme${master.number}.xml`,
1523
+ data: themeXml(`MarkdStage ${master.theme}`),
1524
+ },
1525
+ {
1526
+ name: `ppt/slideMasters/slideMaster${master.number}.xml`,
1527
+ data: slideMasterXml(master, master.layoutNumbers),
1528
+ },
1529
+ {
1530
+ name: `ppt/slideMasters/_rels/slideMaster${master.number}.xml.rels`,
1531
+ data: relationshipsXml([
1532
+ ...master.layoutNumbers.map((layoutNumber, index) => ({
1533
+ id: `rId${index + 1}`,
1534
+ type: REL.slideLayout,
1535
+ target: `../slideLayouts/slideLayout${layoutNumber}.xml`,
1536
+ })),
1537
+ {
1538
+ id: `rId${master.layoutNumbers.length + 1}`,
1539
+ type: REL.theme,
1540
+ target: `../theme/theme${master.number}.xml`,
1541
+ },
1542
+ ]),
1543
+ },
1544
+ ]),
1545
+ ...structure.layouts.flatMap((layout, index) => {
1546
+ const builtLayout = builtLayouts[index];
1547
+ const entries = [
1245
1548
  {
1246
1549
  id: "rId1",
1247
- type: REL.slideLayout,
1248
- target: "../slideLayouts/slideLayout1.xml",
1550
+ type: REL.slideMaster,
1551
+ target: `../slideMasters/slideMaster${layout.masterNumber}.xml`,
1249
1552
  },
1250
- { id: "rId2", type: REL.theme, target: "../theme/theme1.xml" },
1251
- ]),
1252
- },
1253
- { name: "ppt/slideLayouts/slideLayout1.xml", data: slideLayoutXml() },
1254
- {
1255
- name: "ppt/slideLayouts/_rels/slideLayout1.xml.rels",
1256
- data: relationshipsXml([
1553
+ ...builtLayout.imageRelationships.map(({ id, asset }) => ({
1554
+ id,
1555
+ type: REL.image,
1556
+ target: `../media/${asset.mediaPath.split("/").at(-1)}`,
1557
+ })),
1558
+ ];
1559
+ return [
1257
1560
  {
1258
- id: "rId1",
1259
- type: REL.slideMaster,
1260
- target: "../slideMasters/slideMaster1.xml",
1561
+ name: `ppt/slideLayouts/slideLayout${layout.number}.xml`,
1562
+ data: builtLayout.xml,
1261
1563
  },
1262
- ]),
1263
- },
1564
+ {
1565
+ name: `ppt/slideLayouts/_rels/slideLayout${layout.number}.xml.rels`,
1566
+ data: relationshipsXml(entries),
1567
+ },
1568
+ ];
1569
+ }),
1264
1570
  ...(notesSlides.length
1265
1571
  ? [
1266
- { name: "ppt/theme/theme2.xml", data: themeXml() },
1572
+ {
1573
+ name: `ppt/theme/theme${structure.masters.length + 1}.xml`,
1574
+ data: themeXml("MarkdStage Notes"),
1575
+ },
1267
1576
  { name: "ppt/notesMasters/notesMaster1.xml", data: notesMasterXml() },
1268
1577
  {
1269
1578
  name: "ppt/notesMasters/_rels/notesMaster1.xml.rels",
1270
1579
  data: relationshipsXml([
1271
- { id: "rId1", type: REL.theme, target: "../theme/theme2.xml" },
1580
+ {
1581
+ id: "rId1",
1582
+ type: REL.theme,
1583
+ target: `../theme/theme${structure.masters.length + 1}.xml`,
1584
+ },
1272
1585
  ]),
1273
1586
  },
1274
1587
  ]
@@ -1315,6 +1628,27 @@ export function inspectPptxPackage(buffer) {
1315
1628
  const notesNames = [...files.keys()].filter((name) =>
1316
1629
  /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(name),
1317
1630
  );
1631
+ const masterNames = [...files.keys()].filter((name) =>
1632
+ /^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(name),
1633
+ );
1634
+ const layoutNames = [...files.keys()].filter((name) =>
1635
+ /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(name),
1636
+ );
1637
+ const slideLayoutTargets = slideNames.map((name) => {
1638
+ const number = Number(/\d+/.exec(name)[0]);
1639
+ const relationships = files
1640
+ .get(`ppt/slides/_rels/slide${number}.xml.rels`)
1641
+ ?.data.toString("utf8");
1642
+ const target =
1643
+ relationships &&
1644
+ /<Relationship\b[^>]*Type="[^"]*\/slideLayout"[^>]*Target="\.\.\/slideLayouts\/(slideLayout\d+\.xml)"/.exec(
1645
+ relationships,
1646
+ )?.[1];
1647
+ if (!target || !files.has(`ppt/slideLayouts/${target}`)) {
1648
+ throw new Error(`Invalid PowerPoint package: slide ${number} layout is missing`);
1649
+ }
1650
+ return target;
1651
+ });
1318
1652
  const core = files.get("docProps/core.xml")?.data.toString("utf8") || "";
1319
1653
  const title = /<dc:title>([\s\S]*?)<\/dc:title>/.exec(core);
1320
1654
  return {
@@ -1327,6 +1661,9 @@ export function inspectPptxPackage(buffer) {
1327
1661
  })),
1328
1662
  slideCount: slideNames.length,
1329
1663
  notesCount: notesNames.length,
1664
+ masterCount: masterNames.length,
1665
+ layoutCount: layoutNames.length,
1666
+ slideLayoutTargets,
1330
1667
  mediaCount: [...files.keys()].filter((name) => name.startsWith("ppt/media/"))
1331
1668
  .length,
1332
1669
  dimensions: {