@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.
@@ -276,6 +276,12 @@ body.mermaid-loading .mermaid{visibility:hidden;}
276
276
  .architecture-routing-warning span{font-family:"Cascadia Code","Cascadia Mono",Consolas,monospace;
277
277
  font-size:.85em;overflow-wrap:anywhere;}
278
278
  hr{border:0;border-top:1px solid var(--border);margin:.7em 0;}
279
+ /* Flex item margins do not collapse, so regular slide bodies use their gap as
280
+ the single source of vertical spacing. Nested component spacing is unchanged. */
281
+ .deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table):has(+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)),
282
+ .deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table){
283
+ margin-block:0;
284
+ }
279
285
  footer{flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;
280
286
  color:var(--muted);font-size:clamp(12px,1.7vh,16px);
281
287
  margin-top:clamp(12px,2.2vh,26px);border-top:1px solid var(--border);padding-top:12px;}
@@ -656,6 +662,7 @@ body.pptx-artwork-mode [data-pptx-native="table"] *{
656
662
  }
657
663
  body.pptx-artwork-mode li[data-pptx-native="text"]::marker{color:transparent!important;}
658
664
  body.pptx-artwork-mode [data-pptx-native="table"]{visibility:hidden!important;}
665
+ body.pptx-artwork-mode pre[data-pptx-native="code"]{visibility:hidden!important;}
659
666
  body.pptx-artwork-mode img[data-pptx-native="image"]{
660
667
  object-fit:none!important;
661
668
  object-position:99999px 99999px!important;
@@ -674,6 +681,18 @@ body.pptx-artwork-mode svg [data-pptx-native="table"]{
674
681
  }
675
682
  body.pptx-artwork-mode svg path[data-pptx-native="connector"]{marker-end:none!important;}
676
683
  body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
684
+ body.pptx-artwork-mode [data-pptx-shadow-fallback]{box-shadow:none!important;}
685
+ body.pptx-artwork-mode .pptx-fallback-hidden{visibility:hidden!important;}
686
+ body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template){
687
+ background:transparent!important;
688
+ }
689
+ body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)::before{
690
+ display:none!important;
691
+ }
692
+ body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-background,
693
+ body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-logo{
694
+ visibility:hidden!important;
695
+ }
677
696
 
678
697
  /* ===== Headless PDF export ===== */
679
698
  @page{size:13.333333in 7.5in;margin:0;}
@@ -581,6 +581,8 @@ export async function startArchitectureEditorServer({
581
581
  if (
582
582
  [
583
583
  "/renderer/architecture.mjs",
584
+ "/renderer/architecture-contract.mjs",
585
+ "/renderer/architecture-diagnostics.mjs",
584
586
  "/renderer/architecture-edit.mjs",
585
587
  "/renderer/architecture-document.mjs",
586
588
  ].includes(route)
@@ -2,6 +2,7 @@ import { readFile, realpath, stat } from "node:fs/promises";
2
2
  import { isAbsolute, relative, resolve, sep } from "node:path";
3
3
 
4
4
  import { parseArchitecture } from "../renderer/architecture.mjs";
5
+ import { ArchitectureError } from "../renderer/architecture-diagnostics.mjs";
5
6
  import {
6
7
  findArchitectureBlocks,
7
8
  replaceArchitectureBlock,
@@ -85,7 +86,11 @@ export async function readArchitectureSourceTarget(workspaceRoot, sourcePath, bl
85
86
  try {
86
87
  parseArchitecture(block.body);
87
88
  } catch (error) {
88
- throw sourceError("invalid_architecture", error?.message || "Invalid Architecture DSL.");
89
+ if (!(error instanceof ArchitectureError)) throw error;
90
+ throw Object.assign(
91
+ sourceError("invalid_architecture", error.message || "Invalid Architecture DSL."),
92
+ { diagnostic: error.diagnostic, validation: error.validation },
93
+ );
89
94
  }
90
95
  return { ...target, markdown, source: block.body };
91
96
  }
@@ -103,10 +108,13 @@ export function saveArchitectureSource({
103
108
  try {
104
109
  parseArchitecture(source);
105
110
  } catch (error) {
111
+ if (!(error instanceof ArchitectureError)) throw error;
106
112
  return {
107
113
  ok: false,
108
114
  error: "invalid_architecture",
109
115
  message: error?.message || "The diagram is invalid.",
116
+ diagnostic: error.diagnostic,
117
+ validation: error.validation,
110
118
  };
111
119
  }
112
120
 
@@ -453,7 +453,13 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
453
453
  );
454
454
  }
455
455
  const model = evaluated.result?.value;
456
- if (!model || !Array.isArray(model.slides) || model.slides.length !== total) {
456
+ if (
457
+ !model ||
458
+ !Array.isArray(model.masters) ||
459
+ !Array.isArray(model.layouts) ||
460
+ !Array.isArray(model.slides) ||
461
+ model.slides.length !== total
462
+ ) {
457
463
  throw new Error("The renderer returned an invalid PowerPoint export model.");
458
464
  }
459
465
 
@@ -463,26 +469,111 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
463
469
  awaitPromise: true,
464
470
  });
465
471
 
466
- const backgrounds = [];
467
- for (let index = 0; index < total; index += 1) {
472
+ const layoutArtworks = [];
473
+ for (const [index, layout] of model.layouts.entries()) {
474
+ const captureIndex = Number.isInteger(layout.captureIndex)
475
+ ? layout.captureIndex
476
+ : total + index;
468
477
  const screenshot = await cdp.send("Page.captureScreenshot", {
469
478
  format: "png",
470
479
  fromSurface: true,
471
480
  captureBeyondViewport: true,
472
481
  clip: {
473
482
  x: 0,
474
- y: index * 720,
483
+ y: captureIndex * 720,
475
484
  width: 1280,
476
485
  height: 720,
477
486
  scale: 1,
478
487
  },
479
488
  });
480
489
  if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
481
- throw new Error(`Chromium did not return fallback artwork for slide ${index + 1}.`);
490
+ throw new Error(`Chromium did not return artwork for PowerPoint layout ${layout.id}.`);
482
491
  }
483
- backgrounds.push(Buffer.from(screenshot.data, "base64"));
492
+ layoutArtworks.push(Buffer.from(screenshot.data, "base64"));
484
493
  }
485
- return { model, backgrounds };
494
+
495
+ await cdp.send("Runtime.evaluate", {
496
+ expression:
497
+ "document.body.classList.remove('pptx-layout-artwork-mode');" +
498
+ "document.body.classList.add('pptx-slide-artwork-mode');" +
499
+ "new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
500
+ awaitPromise: true,
501
+ });
502
+ await cdp.send("Emulation.setDefaultBackgroundColorOverride", {
503
+ color: { r: 0, g: 0, b: 0, a: 0 },
504
+ });
505
+
506
+ const slideFallbackImages = [];
507
+ for (const [slideIndex, slide] of model.slides.entries()) {
508
+ const images = [];
509
+ const fallbacks = Array.isArray(slide.fallbacks) ? slide.fallbacks : [];
510
+ for (const [fallbackIndex, fallback] of fallbacks.entries()) {
511
+ if (fallback?.artwork === false) continue;
512
+ if (typeof fallback?.captureId !== "string" || !fallback.captureId) {
513
+ throw new Error(
514
+ `PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} is missing its capture id.`,
515
+ );
516
+ }
517
+ const left = Math.max(0, Number(fallback?.x));
518
+ const top = Math.max(0, Number(fallback?.y));
519
+ const right = Math.min(1280, Number(fallback?.x) + Number(fallback?.width));
520
+ const bottom = Math.min(720, Number(fallback?.y) + Number(fallback?.height));
521
+ if (
522
+ ![left, top, right, bottom].every(Number.isFinite) ||
523
+ right <= left ||
524
+ bottom <= top
525
+ ) {
526
+ throw new Error(
527
+ `PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} has invalid bounds.`,
528
+ );
529
+ }
530
+ const bounds = {
531
+ x: left,
532
+ y: top,
533
+ width: right - left,
534
+ height: bottom - top,
535
+ };
536
+ await cdp.send("Runtime.evaluate", {
537
+ expression: `(() => {
538
+ const active = ${JSON.stringify(fallback.captureId)};
539
+ for (const element of document.querySelectorAll("[data-pptx-fallback-ids]")) {
540
+ const ids = (element.getAttribute("data-pptx-fallback-ids") || "").split(/\\s+/);
541
+ element.classList.toggle("pptx-fallback-hidden", !ids.includes(active));
542
+ }
543
+ return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
544
+ })()`,
545
+ awaitPromise: true,
546
+ });
547
+ const screenshot = await cdp.send("Page.captureScreenshot", {
548
+ format: "png",
549
+ fromSurface: true,
550
+ captureBeyondViewport: true,
551
+ clip: {
552
+ x: bounds.x,
553
+ y: slideIndex * 720 + bounds.y,
554
+ width: bounds.width,
555
+ height: bounds.height,
556
+ scale: 1,
557
+ },
558
+ });
559
+ if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
560
+ throw new Error(
561
+ `Chromium did not return fallback artwork ${fallbackIndex + 1} for slide ${slideIndex + 1}.`,
562
+ );
563
+ }
564
+ images.push({
565
+ fallbackIndex,
566
+ ...bounds,
567
+ data: Buffer.from(screenshot.data, "base64"),
568
+ });
569
+ }
570
+ slideFallbackImages.push(images);
571
+ }
572
+ await cdp.send("Runtime.evaluate", {
573
+ expression:
574
+ 'document.querySelectorAll(".pptx-fallback-hidden").forEach(element => element.classList.remove("pptx-fallback-hidden"));',
575
+ });
576
+ return { model, layoutArtworks, slideFallbackImages };
486
577
  } finally {
487
578
  await closeCdpOutputPage(cdp, child);
488
579
  }
@@ -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))(;base64)?,([\s\S]*)$/i.exec(source);
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 GIF data URLs can be embedded in PowerPoint.");
86
+ throw new Error("Only PNG, JPEG, GIF, or SVG data URLs can be embedded in PowerPoint.");
87
87
  }
88
- const data = match[2]
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(decodeURIComponent(match[3]), "binary");
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
- backgrounds,
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(backgrounds) || backgrounds.length !== model.slides.length) {
152
- throw new Error("PowerPoint fallback artwork does not match the slide count.");
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
- for (const [index, background] of backgrounds.entries()) {
159
- if (!Buffer.isBuffer(background)) {
160
- throw new Error(`PowerPoint fallback artwork for slide ${index + 1} is invalid.`);
179
+ const addPngAsset = (id, data, label) => {
180
+ if (!Buffer.isBuffer(data)) {
181
+ throw new Error(`${label} is invalid.`);
161
182
  }
162
- ensurePptxAssetSize(background, `slide ${index + 1} fallback artwork`, totalAssetBytes);
163
- totalAssetBytes += background.length;
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: `markdstage-background-${index + 1}`,
189
+ id,
166
190
  contentType: "image/png",
167
- data: background,
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
- const source = sourceElement.src;
186
- let assetId = sourceAssets.get(source);
187
- if (!assetId) {
188
- const loaded = await loadPptxImage(inst, source, fetchImpl, totalAssetBytes);
189
- totalAssetBytes += loaded.data.length;
190
- assetId = `markdstage-image-${sourceAssets.size + 1}`;
191
- sourceAssets.set(source, assetId);
192
- assets.push({ id: assetId, ...loaded });
193
- }
194
- const { src: _src, source: _source, ...image } = sourceElement;
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
- (image.fit === "contain" || image.fit === "scale-down") &&
197
- image.naturalWidth > 0 &&
198
- image.naturalHeight > 0
334
+ !capture ||
335
+ !Number.isInteger(capture.fallbackIndex) ||
336
+ captureByFallback.has(capture.fallbackIndex)
199
337
  ) {
200
- const scale = Math.min(
201
- image.width / image.naturalWidth,
202
- image.height / image.naturalHeight,
203
- image.fit === "scale-down" ? 1 : Number.POSITIVE_INFINITY,
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
- elements.push({ ...image, assetId });
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
- backgroundAssetId: `markdstage-background-${slideIndex + 1}`,
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) {
@@ -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, backgrounds } = await runBrowser(
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(inst, model, backgrounds);
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
- slideIndex,
620
- page: slideIndex + 1,
621
- ...fallback,
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,