@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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
10
10
|
import { basename, extname, join } from "node:path";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { getOutputSnapshotSlides } from "../deck-state.mjs";
|
|
14
14
|
import { normalizeTheme } from "../renderer/theme.mjs";
|
|
15
15
|
import { MarkdStageError } from "./errors.mjs";
|
|
@@ -81,13 +81,21 @@ export function createOutputJob(snapshot, kind) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function decodeDataImage(source) {
|
|
84
|
-
const match = /^data:(image\/(?:png|jpeg|gif))(
|
|
84
|
+
const match = /^data:(image\/(?:png|jpeg|gif|svg\+xml))((?:;[^,]*)*),([\s\S]*)$/i.exec(source);
|
|
85
85
|
if (!match) {
|
|
86
|
-
throw new Error("Only PNG, JPEG, or
|
|
86
|
+
throw new Error("Only PNG, JPEG, GIF, or SVG data URLs can be embedded in PowerPoint.");
|
|
87
87
|
}
|
|
88
|
-
const
|
|
88
|
+
const parameters = match[2]
|
|
89
|
+
.split(";")
|
|
90
|
+
.map((parameter) => parameter.trim().toLowerCase())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
const base64 = parameters.includes("base64");
|
|
93
|
+
const data = base64
|
|
89
94
|
? Buffer.from(match[3], "base64")
|
|
90
|
-
: Buffer.from(
|
|
95
|
+
: Buffer.from(
|
|
96
|
+
decodeURIComponent(match[3]),
|
|
97
|
+
match[1].toLowerCase() === "image/svg+xml" ? "utf8" : "binary",
|
|
98
|
+
);
|
|
91
99
|
return { data, contentType: match[1].toLowerCase() };
|
|
92
100
|
}
|
|
93
101
|
|
|
@@ -126,7 +134,7 @@ async function loadPptxImage(inst, source, fetchImpl, currentTotal) {
|
|
|
126
134
|
const data = Buffer.from(await response.arrayBuffer());
|
|
127
135
|
ensurePptxAssetSize(data, source, currentTotal);
|
|
128
136
|
const responseType = response.headers.get("content-type")?.split(";")[0].trim().toLowerCase();
|
|
129
|
-
const contentType = ["image/png", "image/jpeg", "image/gif"].includes(responseType)
|
|
137
|
+
const contentType = ["image/png", "image/jpeg", "image/gif", "image/svg+xml"].includes(responseType)
|
|
130
138
|
? responseType
|
|
131
139
|
: undefined;
|
|
132
140
|
return { data, contentType };
|
|
@@ -135,7 +143,8 @@ async function loadPptxImage(inst, source, fetchImpl, currentTotal) {
|
|
|
135
143
|
export async function preparePptxPackageModel(
|
|
136
144
|
inst,
|
|
137
145
|
model,
|
|
138
|
-
|
|
146
|
+
layoutArtworks,
|
|
147
|
+
slideFallbackImages,
|
|
139
148
|
fetchImpl = fetch,
|
|
140
149
|
) {
|
|
141
150
|
if (
|
|
@@ -143,29 +152,148 @@ export async function preparePptxPackageModel(
|
|
|
143
152
|
model.version !== 1 ||
|
|
144
153
|
model.width !== PPTX_DIMENSIONS.widthPx ||
|
|
145
154
|
model.height !== PPTX_DIMENSIONS.heightPx ||
|
|
155
|
+
!Array.isArray(model.masters) ||
|
|
156
|
+
model.masters.length === 0 ||
|
|
157
|
+
!Array.isArray(model.layouts) ||
|
|
158
|
+
model.layouts.length === 0 ||
|
|
146
159
|
!Array.isArray(model.slides) ||
|
|
147
160
|
model.slides.length === 0
|
|
148
161
|
) {
|
|
149
162
|
throw new Error("The renderer returned an unsupported PowerPoint export model.");
|
|
150
163
|
}
|
|
151
|
-
if (!Array.isArray(
|
|
152
|
-
throw new Error("PowerPoint
|
|
164
|
+
if (!Array.isArray(layoutArtworks) || layoutArtworks.length !== model.layouts.length) {
|
|
165
|
+
throw new Error("PowerPoint layout artwork does not match the layout count.");
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
!Array.isArray(slideFallbackImages) ||
|
|
169
|
+
slideFallbackImages.length !== model.slides.length ||
|
|
170
|
+
slideFallbackImages.some((images) => !Array.isArray(images))
|
|
171
|
+
) {
|
|
172
|
+
throw new Error("PowerPoint fallback images do not match the slide count.");
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
const assets = [];
|
|
156
176
|
const sourceAssets = new Map();
|
|
177
|
+
const pngAssets = new Map();
|
|
157
178
|
let totalAssetBytes = 0;
|
|
158
|
-
|
|
159
|
-
if (!Buffer.isBuffer(
|
|
160
|
-
throw new Error(
|
|
179
|
+
const addPngAsset = (id, data, label) => {
|
|
180
|
+
if (!Buffer.isBuffer(data)) {
|
|
181
|
+
throw new Error(`${label} is invalid.`);
|
|
161
182
|
}
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
const key = createHash("sha256").update(data).digest("hex");
|
|
184
|
+
const existing = pngAssets.get(key);
|
|
185
|
+
if (existing) return existing;
|
|
186
|
+
ensurePptxAssetSize(data, label, totalAssetBytes);
|
|
187
|
+
totalAssetBytes += data.length;
|
|
164
188
|
assets.push({
|
|
165
|
-
id
|
|
189
|
+
id,
|
|
166
190
|
contentType: "image/png",
|
|
167
|
-
data
|
|
191
|
+
data,
|
|
168
192
|
});
|
|
193
|
+
pngAssets.set(key, id);
|
|
194
|
+
return id;
|
|
195
|
+
};
|
|
196
|
+
const prepareImage = async (sourceElement) => {
|
|
197
|
+
const source = sourceElement.src;
|
|
198
|
+
let assetId = sourceAssets.get(source);
|
|
199
|
+
if (!assetId) {
|
|
200
|
+
const loaded = await loadPptxImage(inst, source, fetchImpl, totalAssetBytes);
|
|
201
|
+
totalAssetBytes += loaded.data.length;
|
|
202
|
+
assetId = `markdstage-image-${sourceAssets.size + 1}`;
|
|
203
|
+
sourceAssets.set(source, assetId);
|
|
204
|
+
assets.push({ id: assetId, ...loaded });
|
|
205
|
+
}
|
|
206
|
+
const { src: _src, source: _source, ...image } = sourceElement;
|
|
207
|
+
if (
|
|
208
|
+
(image.fit === "contain" || image.fit === "scale-down") &&
|
|
209
|
+
image.naturalWidth > 0 &&
|
|
210
|
+
image.naturalHeight > 0
|
|
211
|
+
) {
|
|
212
|
+
const scale = Math.min(
|
|
213
|
+
image.width / image.naturalWidth,
|
|
214
|
+
image.height / image.naturalHeight,
|
|
215
|
+
image.fit === "scale-down" ? 1 : Number.POSITIVE_INFINITY,
|
|
216
|
+
);
|
|
217
|
+
const width = image.naturalWidth * scale;
|
|
218
|
+
const height = image.naturalHeight * scale;
|
|
219
|
+
image.x += (image.width - width) / 2;
|
|
220
|
+
image.y += (image.height - height) / 2;
|
|
221
|
+
image.width = width;
|
|
222
|
+
image.height = height;
|
|
223
|
+
}
|
|
224
|
+
return { ...image, assetId };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const layouts = [];
|
|
228
|
+
const layoutById = new Map();
|
|
229
|
+
for (const [index, sourceLayout] of model.layouts.entries()) {
|
|
230
|
+
if (
|
|
231
|
+
!sourceLayout ||
|
|
232
|
+
typeof sourceLayout.id !== "string" ||
|
|
233
|
+
!sourceLayout.id ||
|
|
234
|
+
typeof sourceLayout.name !== "string" ||
|
|
235
|
+
!sourceLayout.name ||
|
|
236
|
+
typeof sourceLayout.theme !== "string" ||
|
|
237
|
+
!sourceLayout.theme
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(`PowerPoint layout ${index + 1} is invalid.`);
|
|
240
|
+
}
|
|
241
|
+
if (layoutById.has(sourceLayout.id)) {
|
|
242
|
+
throw new Error(`PowerPoint layout id is duplicated: ${sourceLayout.id}`);
|
|
243
|
+
}
|
|
244
|
+
const artworkAssetId = addPngAsset(
|
|
245
|
+
`markdstage-layout-${index + 1}`,
|
|
246
|
+
layoutArtworks[index],
|
|
247
|
+
`PowerPoint artwork for layout ${sourceLayout.id}`,
|
|
248
|
+
);
|
|
249
|
+
const sourceElements = sourceLayout.elements ?? [];
|
|
250
|
+
if (!Array.isArray(sourceElements) || sourceElements.some((element) => element?.type !== "image")) {
|
|
251
|
+
throw new Error(`PowerPoint layout ${sourceLayout.id} has an invalid element list.`);
|
|
252
|
+
}
|
|
253
|
+
const elements = [];
|
|
254
|
+
for (const sourceElement of sourceElements) {
|
|
255
|
+
elements.push(await prepareImage(sourceElement));
|
|
256
|
+
}
|
|
257
|
+
const layout = {
|
|
258
|
+
id: sourceLayout.id,
|
|
259
|
+
name: sourceLayout.name,
|
|
260
|
+
theme: sourceLayout.theme,
|
|
261
|
+
artworkAssetId,
|
|
262
|
+
elements,
|
|
263
|
+
};
|
|
264
|
+
layoutById.set(layout.id, layout);
|
|
265
|
+
layouts.push(layout);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const masters = model.masters.map((sourceMaster, index) => {
|
|
269
|
+
if (
|
|
270
|
+
!sourceMaster ||
|
|
271
|
+
typeof sourceMaster.id !== "string" ||
|
|
272
|
+
!sourceMaster.id ||
|
|
273
|
+
typeof sourceMaster.theme !== "string" ||
|
|
274
|
+
!sourceMaster.theme ||
|
|
275
|
+
!Array.isArray(sourceMaster.layoutIds) ||
|
|
276
|
+
sourceMaster.layoutIds.length === 0
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(`PowerPoint master ${index + 1} is invalid.`);
|
|
279
|
+
}
|
|
280
|
+
const layoutIds = sourceMaster.layoutIds.map((layoutId) => {
|
|
281
|
+
const layout = layoutById.get(layoutId);
|
|
282
|
+
if (!layout || layout.theme !== sourceMaster.theme) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`PowerPoint master ${sourceMaster.id} references invalid layout ${layoutId}.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return layoutId;
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
id: sourceMaster.id,
|
|
291
|
+
theme: sourceMaster.theme,
|
|
292
|
+
layoutIds,
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
if (new Set(masters.map((master) => master.id)).size !== masters.length) {
|
|
296
|
+
throw new Error("PowerPoint master ids must be unique.");
|
|
169
297
|
}
|
|
170
298
|
|
|
171
299
|
const slides = [];
|
|
@@ -173,6 +301,12 @@ export async function preparePptxPackageModel(
|
|
|
173
301
|
if (!sourceSlide || !Array.isArray(sourceSlide.elements)) {
|
|
174
302
|
throw new Error(`PowerPoint slide ${slideIndex + 1} has an invalid element list.`);
|
|
175
303
|
}
|
|
304
|
+
if (
|
|
305
|
+
typeof sourceSlide.layoutId !== "string" ||
|
|
306
|
+
!layoutById.has(sourceSlide.layoutId)
|
|
307
|
+
) {
|
|
308
|
+
throw new Error(`PowerPoint slide ${slideIndex + 1} references an invalid layout.`);
|
|
309
|
+
}
|
|
176
310
|
if (sourceSlide.notes !== undefined && typeof sourceSlide.notes !== "string") {
|
|
177
311
|
throw new Error(`PowerPoint slide ${slideIndex + 1} has invalid speaker notes.`);
|
|
178
312
|
}
|
|
@@ -182,42 +316,75 @@ export async function preparePptxPackageModel(
|
|
|
182
316
|
elements.push(sourceElement);
|
|
183
317
|
continue;
|
|
184
318
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
319
|
+
elements.push(await prepareImage(sourceElement));
|
|
320
|
+
}
|
|
321
|
+
const fallbacks = Array.isArray(sourceSlide.fallbacks) ? sourceSlide.fallbacks : [];
|
|
322
|
+
const expectedFallbackIndexes = fallbacks
|
|
323
|
+
.map((fallback, fallbackIndex) => ({ fallback, fallbackIndex }))
|
|
324
|
+
.filter(({ fallback }) => fallback?.artwork !== false);
|
|
325
|
+
const captures = slideFallbackImages[slideIndex];
|
|
326
|
+
if (captures.length !== expectedFallbackIndexes.length) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`PowerPoint fallback images do not match slide ${slideIndex + 1}.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const captureByFallback = new Map();
|
|
332
|
+
for (const capture of captures) {
|
|
195
333
|
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
334
|
+
!capture ||
|
|
335
|
+
!Number.isInteger(capture.fallbackIndex) ||
|
|
336
|
+
captureByFallback.has(capture.fallbackIndex)
|
|
199
337
|
) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
338
|
+
throw new Error(`PowerPoint fallback image for slide ${slideIndex + 1} is invalid.`);
|
|
339
|
+
}
|
|
340
|
+
captureByFallback.set(capture.fallbackIndex, capture);
|
|
341
|
+
}
|
|
342
|
+
const fallbackElements = [];
|
|
343
|
+
for (const { fallback, fallbackIndex } of expectedFallbackIndexes) {
|
|
344
|
+
const capture = captureByFallback.get(fallbackIndex);
|
|
345
|
+
if (!capture) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`PowerPoint fallback image ${fallbackIndex + 1} for slide ${slideIndex + 1} is missing.`,
|
|
204
348
|
);
|
|
205
|
-
const width = image.naturalWidth * scale;
|
|
206
|
-
const height = image.naturalHeight * scale;
|
|
207
|
-
image.x += (image.width - width) / 2;
|
|
208
|
-
image.y += (image.height - height) / 2;
|
|
209
|
-
image.width = width;
|
|
210
|
-
image.height = height;
|
|
211
349
|
}
|
|
212
|
-
|
|
350
|
+
const assetId = addPngAsset(
|
|
351
|
+
`markdstage-slide-${slideIndex + 1}-fallback-${fallbackIndex + 1}`,
|
|
352
|
+
capture.data,
|
|
353
|
+
`PowerPoint fallback image ${fallbackIndex + 1} for slide ${slideIndex + 1}`,
|
|
354
|
+
);
|
|
355
|
+
fallbackElements.push({
|
|
356
|
+
type: "image",
|
|
357
|
+
path: fallback.path,
|
|
358
|
+
name: `${fallback.type || "Fallback"} artwork`,
|
|
359
|
+
x: capture.x,
|
|
360
|
+
y: capture.y,
|
|
361
|
+
width: capture.width,
|
|
362
|
+
height: capture.height,
|
|
363
|
+
fit: "fill",
|
|
364
|
+
opacity: 1,
|
|
365
|
+
...(Number.isFinite(fallback.zOrder) ? { zOrder: fallback.zOrder } : {}),
|
|
366
|
+
assetId,
|
|
367
|
+
});
|
|
213
368
|
}
|
|
369
|
+
const orderedElements = [...fallbackElements, ...elements]
|
|
370
|
+
.map((element, elementIndex) => ({ element, elementIndex }))
|
|
371
|
+
.sort((left, right) => {
|
|
372
|
+
const leftOrder = Number.isFinite(left.element.zOrder)
|
|
373
|
+
? left.element.zOrder
|
|
374
|
+
: Number.POSITIVE_INFINITY;
|
|
375
|
+
const rightOrder = Number.isFinite(right.element.zOrder)
|
|
376
|
+
? right.element.zOrder
|
|
377
|
+
: Number.POSITIVE_INFINITY;
|
|
378
|
+
return leftOrder - rightOrder || left.elementIndex - right.elementIndex;
|
|
379
|
+
})
|
|
380
|
+
.map(({ element }) => element);
|
|
214
381
|
slides.push({
|
|
215
|
-
|
|
382
|
+
layoutId: sourceSlide.layoutId,
|
|
216
383
|
...(sourceSlide.notes ? { notes: sourceSlide.notes } : {}),
|
|
217
|
-
elements,
|
|
384
|
+
elements: orderedElements,
|
|
218
385
|
});
|
|
219
386
|
}
|
|
220
|
-
return { slides, assets };
|
|
387
|
+
return { masters, layouts, slides, assets };
|
|
221
388
|
}
|
|
222
389
|
|
|
223
390
|
async function runLayoutInspectionJob(inst, snapshot, browser) {
|
|
@@ -497,14 +664,14 @@ export async function exportPdf(inst, requestedPath, requestedTheme) {
|
|
|
497
664
|
profileDir = await mkdtemp(join(tmpdir(), "markdstage-pdf-"));
|
|
498
665
|
const outputBase = basename(outputPath, extname(outputPath)) || "markdstage";
|
|
499
666
|
temporaryOutputPath = join(outputParent, `.${outputBase}.${token}.tmp.pdf`);
|
|
500
|
-
|
|
667
|
+
const exportJob = createOutputJob(snapshot, "pdf");
|
|
668
|
+
inst.exportJobs.set(token, exportJob);
|
|
501
669
|
|
|
502
670
|
const pageUrl = pageUrlFor(inst, { print: 1, token });
|
|
503
|
-
await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir);
|
|
504
|
-
|
|
505
|
-
if (exportJob?.status !== "ready") {
|
|
671
|
+
await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir, exportJob);
|
|
672
|
+
if (exportJob.status !== "ready") {
|
|
506
673
|
throw new Error(
|
|
507
|
-
exportJob
|
|
674
|
+
exportJob.error || "The print renderer did not finish before PDF generation.",
|
|
508
675
|
);
|
|
509
676
|
}
|
|
510
677
|
const bytes = await verifyPdf(temporaryOutputPath);
|
|
@@ -585,14 +752,19 @@ export async function exportPptx(
|
|
|
585
752
|
inst.exportJobs.set(token, job);
|
|
586
753
|
|
|
587
754
|
const pageUrl = pageUrlFor(inst, { pptx: 1, token });
|
|
588
|
-
const { model,
|
|
755
|
+
const { model, layoutArtworks, slideFallbackImages } = await runBrowser(
|
|
589
756
|
browser,
|
|
590
757
|
pageUrl,
|
|
591
758
|
profileDir,
|
|
592
759
|
job,
|
|
593
760
|
snapshot.slides.length,
|
|
594
761
|
);
|
|
595
|
-
const packageModel = await prepareModel(
|
|
762
|
+
const packageModel = await prepareModel(
|
|
763
|
+
inst,
|
|
764
|
+
model,
|
|
765
|
+
layoutArtworks,
|
|
766
|
+
slideFallbackImages,
|
|
767
|
+
);
|
|
596
768
|
const buffer = buildPackage({
|
|
597
769
|
title: model.slides[0]?.title || outputBase,
|
|
598
770
|
...packageModel,
|
|
@@ -605,6 +777,8 @@ export async function exportPptx(
|
|
|
605
777
|
!packageSummary.valid ||
|
|
606
778
|
packageSummary.slideCount !== snapshot.slides.length ||
|
|
607
779
|
packageSummary.notesCount !== expectedNotes ||
|
|
780
|
+
packageSummary.masterCount !== model.masters.length ||
|
|
781
|
+
packageSummary.layoutCount !== model.layouts.length ||
|
|
608
782
|
packageSummary.dimensions.widthEmu !== PPTX_DIMENSIONS.widthEmu ||
|
|
609
783
|
packageSummary.dimensions.heightEmu !== PPTX_DIMENSIONS.heightEmu
|
|
610
784
|
) {
|
|
@@ -615,11 +789,19 @@ export async function exportPptx(
|
|
|
615
789
|
await rename(temporaryOutputPath, outputPath);
|
|
616
790
|
temporaryOutputPath = "";
|
|
617
791
|
const fallbacks = model.slides.flatMap((slide, slideIndex) =>
|
|
618
|
-
(Array.isArray(slide.fallbacks) ? slide.fallbacks : []).map((fallback) =>
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
792
|
+
(Array.isArray(slide.fallbacks) ? slide.fallbacks : []).map((fallback) => {
|
|
793
|
+
const {
|
|
794
|
+
artwork: _artwork,
|
|
795
|
+
captureId: _captureId,
|
|
796
|
+
zOrder: _zOrder,
|
|
797
|
+
...reportedFallback
|
|
798
|
+
} = fallback;
|
|
799
|
+
return {
|
|
800
|
+
slideIndex,
|
|
801
|
+
page: slideIndex + 1,
|
|
802
|
+
...reportedFallback,
|
|
803
|
+
};
|
|
804
|
+
}),
|
|
623
805
|
);
|
|
624
806
|
logFor(
|
|
625
807
|
inst,
|