@markdstage/markdstage 2.6.0 → 3.1.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/README.md +6 -6
- package/package.json +1 -1
- package/shared/README.md +54 -20
- package/shared/markdstage-guide.mjs +8 -2
- package/shared/renderer/renderer.js +713 -64
- package/shared/renderer/slides.css +33 -0
- package/shared/runtime/browser.mjs +127 -111
- package/shared/runtime/output.mjs +237 -55
- package/shared/runtime/pptx-package.mjs +404 -67
- package/src/cli.mjs +13 -13
- package/src/commands/present.mjs +4 -2
- package/src/skills.mjs +6 -6
|
@@ -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
|
|
199
|
+
function colorChoiceXml(value, path, opacity = 1) {
|
|
199
200
|
const color = colorOf(value, path);
|
|
200
|
-
if (!color) return
|
|
201
|
+
if (!color) return null;
|
|
201
202
|
const alpha = Math.round(color.alpha * opacity * 100000);
|
|
202
|
-
return `<a:
|
|
203
|
+
return `<a:srgbClr val="${color.hex}">${
|
|
203
204
|
alpha < 100000 ? `<a:alpha val="${alpha}"/>` : ""
|
|
204
|
-
}</a:srgbClr
|
|
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
|
-
|
|
418
|
+
!bulletValue
|
|
394
419
|
? paragraph.bullet
|
|
395
|
-
:
|
|
396
|
-
|
|
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
|
|
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(
|
|
604
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
864
|
+
const artworkAssetId = slide.artworkAssetId ?? slide.backgroundAssetId;
|
|
865
|
+
if (artworkAssetId !== undefined) {
|
|
811
866
|
const asset = requireAsset(
|
|
812
867
|
assets,
|
|
813
|
-
|
|
814
|
-
|
|
868
|
+
artworkAssetId,
|
|
869
|
+
slide.artworkAssetId !== undefined
|
|
870
|
+
? `${path}.artworkAssetId`
|
|
871
|
+
: `${path}.backgroundAssetId`,
|
|
815
872
|
);
|
|
816
873
|
if (asset.contentType !== "image/png") {
|
|
817
|
-
fail(`${path}.
|
|
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(
|
|
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
|
-
?
|
|
973
|
+
? `<Override PartName="/ppt/theme/theme${masterCount + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>`
|
|
908
974
|
: "";
|
|
909
|
-
|
|
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 +
|
|
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 +
|
|
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
|
|
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:
|
|
1008
|
+
...Array.from({ length: masterCount }, (_, index) => ({
|
|
1009
|
+
id: `rId${index + 1}`,
|
|
929
1010
|
type: REL.slideMaster,
|
|
930
|
-
target:
|
|
931
|
-
},
|
|
1011
|
+
target: `slideMasters/slideMaster${index + 1}.xml`,
|
|
1012
|
+
})),
|
|
932
1013
|
...Array.from({ length: slideCount }, (_, index) => ({
|
|
933
|
-
id: `rId${index +
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
965
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1514
|
+
data: presentationRelsXml(
|
|
1515
|
+
structure.masters.length,
|
|
1516
|
+
slides.length,
|
|
1517
|
+
notesSlides.length,
|
|
1518
|
+
),
|
|
1239
1519
|
},
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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.
|
|
1248
|
-
target:
|
|
1550
|
+
type: REL.slideMaster,
|
|
1551
|
+
target: `../slideMasters/slideMaster${layout.masterNumber}.xml`,
|
|
1249
1552
|
},
|
|
1250
|
-
{ id
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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: {
|