@markdstage/markdstage 3.4.0 → 3.8.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.
@@ -6,10 +6,13 @@ import { sceneToPptxElements } from "./scene-pptx.mjs";
6
6
  import { attachArchitectureEditor } from "./architecture-editor.mjs";
7
7
  import {
8
8
  DEFAULT_THEME,
9
+ mermaidC4ThemeVariables,
9
10
  mermaidThemeVariables,
10
11
  normalizeTheme,
11
12
  parseFrontMatter,
13
+ resolveThemeBackground,
12
14
  } from "./theme.mjs";
15
+ import { parseSlideBackground } from "./slide-background.mjs";
13
16
  import {
14
17
  extractSpeakerNotes,
15
18
  speakerNotesToPlainText,
@@ -583,13 +586,19 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
583
586
  return Promise.resolve();
584
587
  }
585
588
  try {
586
- const themeVariables = mermaidThemeVariables(getComputedStyle(deckEl));
589
+ const sources = [...nodes].map((node) => node.textContent || "");
590
+ const themeVariables = mermaidThemeVariables(
591
+ getComputedStyle(deckEl),
592
+ (value) => resolveModelColor(value, deckEl),
593
+ );
594
+ const c4ThemeVariables = mermaidC4ThemeVariables(themeVariables);
587
595
  const serializedThemeVariables = JSON.stringify(themeVariables);
588
596
  if (serializedThemeVariables !== lastMermaidThemeVariables) {
589
597
  window.mermaid.initialize({
590
598
  startOnLoad: false,
591
599
  theme: "base",
592
600
  themeVariables,
601
+ c4: c4ThemeVariables,
593
602
  securityLevel: "strict",
594
603
  });
595
604
  lastMermaidThemeVariables = serializedThemeVariables;
@@ -601,11 +610,60 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
601
610
  const source = host.querySelector("svg");
602
611
  if (!source || source.hasAttribute("data-scene-backend")) continue;
603
612
  try {
613
+ repairC4ThemeDefaults(source, sources[index], themeVariables);
604
614
  renderMermaidScene(source, deckEl, index);
605
615
  } catch (e) {
606
616
  console.error("Mermaid scene render failed", e);
607
617
  }
608
618
  }
619
+
620
+ // Mermaid 11.15.0 does not expose theme variables for C4 boundary/relation
621
+ // paint or its arrow markers. Repair only those known renderer defaults, and
622
+ // leave diagrams with explicit C4 style updates untouched so source colors
623
+ // remain authoritative.
624
+ function repairC4ThemeDefaults(svg, source, themeVariables) {
625
+ if (svg.getAttribute("aria-roledescription") !== "c4") return;
626
+ const commands = source.replace(/^\s*%%.*$/gm, "");
627
+ const hasElementStyles = /\bUpdateElementStyle\s*\(/i.test(commands);
628
+ const hasRelationStyles = /\bUpdateRelStyle\s*\(/i.test(commands);
629
+ const boundaryGroups = new Set(
630
+ [...svg.querySelectorAll('rect[stroke-dasharray="7.0,7.0"], rect[fill="none"]')]
631
+ .map((element) => element.parentElement),
632
+ );
633
+ const relationGroups = new Set(
634
+ [...svg.querySelectorAll("[marker-end], [marker-start]")].map((element) => element.parentElement),
635
+ );
636
+ const boundaryPaint = new Set(["#444", "#444444", "rgb(68, 68, 68)"]);
637
+ const relationPaint = new Set(["#444", "#444444", "rgb(68, 68, 68)"]);
638
+ const setDefaultPaint = (element, property, defaults, value) => {
639
+ const current = element.getAttribute(property)?.trim().toLowerCase();
640
+ if (!defaults.has(current)) return;
641
+ element.setAttribute(property, value);
642
+ if (element.style?.getPropertyValue(property)) {
643
+ element.style.setProperty(property, value);
644
+ }
645
+ };
646
+
647
+ for (const element of svg.querySelectorAll("rect, line, path, text")) {
648
+ const parent = element.parentElement;
649
+ if (!hasElementStyles && boundaryGroups.has(parent)) {
650
+ setDefaultPaint(element, "stroke", boundaryPaint, themeVariables.lineColor);
651
+ setDefaultPaint(element, "fill", boundaryPaint, themeVariables.textColor);
652
+ }
653
+ if (!hasRelationStyles && relationGroups.has(parent)) {
654
+ setDefaultPaint(element, "stroke", relationPaint, themeVariables.lineColor);
655
+ setDefaultPaint(element, "fill", relationPaint, themeVariables.textColor);
656
+ }
657
+ }
658
+ if (!hasRelationStyles) {
659
+ for (const marker of svg.querySelectorAll("marker")) {
660
+ for (const element of marker.querySelectorAll("path, polygon, line")) {
661
+ setDefaultPaint(element, "stroke", new Set(["black", "#000", "#000000", "rgb(0, 0, 0)"]), themeVariables.lineColor);
662
+ setDefaultPaint(element, "fill", new Set([undefined, "black", "#000", "#000000", "rgb(0, 0, 0)"]), themeVariables.lineColor);
663
+ }
664
+ }
665
+ }
666
+ }
609
667
  })
610
668
  .finally(reveal);
611
669
  } catch (e) {
@@ -615,26 +673,28 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
615
673
  }
616
674
  }
617
675
 
676
+ function withoutMermaidLoadingVeil(callback) {
677
+ const loading = document.body.classList.contains("mermaid-loading");
678
+ if (loading) document.body.classList.remove("mermaid-loading");
679
+ try {
680
+ return callback();
681
+ } finally {
682
+ if (loading) document.body.classList.add("mermaid-loading");
683
+ }
684
+ }
685
+
618
686
  function renderMermaidScene(svg, deck, blockIndex) {
619
- const result = mermaidSvgToScene(svg, {
620
- path: `mermaid[${blockIndex}]`,
621
- deck,
622
- includeSourceElements: true,
623
- resolveColor: (value) => resolveModelColor(value, deck),
624
- });
687
+ const result = withoutMermaidLoadingVeil(() =>
688
+ mermaidSvgToScene(svg, {
689
+ path: `mermaid[${blockIndex}]`,
690
+ deck,
691
+ includeSourceElements: true,
692
+ resolveColor: (value) => resolveModelColor(value, deck),
693
+ }),
694
+ );
625
695
  let { scene } = result;
626
- const computedStyle = (element) => {
627
- const style = getComputedStyle(element);
628
- return {
629
- getPropertyValue(property) {
630
- // The loading veil is inherited by every SVG descendant, not diagram style.
631
- if (property === "visibility" && document.body.classList.contains("mermaid-loading")) {
632
- return element.style?.visibility || element.getAttribute("visibility") || "";
633
- }
634
- return style.getPropertyValue(property);
635
- },
636
- };
637
- };
696
+ const capture = (element, options) =>
697
+ withoutMermaidLoadingVeil(() => captureSvgTree(element, options));
638
698
  const slots = new Map();
639
699
  try {
640
700
  for (const [index, node] of scene.nodes.entries()) {
@@ -657,7 +717,7 @@ function renderMermaidScene(svg, deck, blockIndex) {
657
717
  node.meta = { ...node.meta, svgOwner: slots.get(source) };
658
718
  continue;
659
719
  }
660
- node.meta = { ...node.meta, svg: captureSvgTree(source, { computedStyle }) };
720
+ node.meta = { ...node.meta, svg: capture(source) };
661
721
  slots.set(source, index);
662
722
  }
663
723
  for (const [source, index] of slots) {
@@ -676,14 +736,14 @@ function renderMermaidScene(svg, deck, blockIndex) {
676
736
  kind: "fallback", sourcePath: "svg", z: 0,
677
737
  bounds: { x: 0, y: 0, width: scene.width, height: scene.height },
678
738
  capability: { pptx: "fallback", reason }, reason,
679
- meta: { svg: captureSvgTree(svg, { computedStyle }) },
739
+ meta: { svg: capture(svg) },
680
740
  }] };
681
741
  slots.clear();
682
742
  slots.set(svg, 0);
683
743
  }
684
744
  const template = slots.has(svg)
685
745
  ? { sceneNode: slots.get(svg) }
686
- : captureSvgTree(svg, { slots, computedStyle });
746
+ : capture(svg, { slots });
687
747
  scene.meta = { ...scene.meta, svgRoot: template };
688
748
  svg.replaceWith(sceneToSvg(scene, { document, template }));
689
749
  }
@@ -698,7 +758,12 @@ function moveLeadingSlideTitle(header, bodyEl, specialLayout) {
698
758
  return title;
699
759
  }
700
760
 
701
- function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
761
+ function createSlide(
762
+ markdown,
763
+ fallbackTheme,
764
+ themeLocked = deckThemeLocked,
765
+ { backgroundImage: backgroundOverride } = {},
766
+ ) {
702
767
  const placeholder = !nonEmpty(markdown);
703
768
  const md = placeholder ? PLACEHOLDER : markdown;
704
769
  const { meta, body: rawBody } = splitFrontMatter(md);
@@ -721,6 +786,11 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
721
786
  // as well as <html> so print mode can render differently themed pages together.
722
787
  const theme = normalizeTheme(themeLocked ? fallbackTheme : meta.theme || fallbackTheme);
723
788
  const themeMetadata = theme === "custom" ? customThemeMeta : null;
789
+ const backgroundImage = parseSlideBackground(backgroundOverride ?? meta["background-image"]);
790
+ const backgroundUrl = backgroundImage
791
+ ? localAssetUrl(backgroundImage.replace(/^\/assets\//, "/background-assets/")
792
+ .split("/").map(encodeURIComponent).join("/"))
793
+ : "";
724
794
 
725
795
  const deck = document.createElement("div");
726
796
  deck.className = "deck";
@@ -732,13 +802,18 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
732
802
  if (placeholder) deck.classList.add("markdstage-placeholder");
733
803
  if (sizeMode !== "auto") setSizeLevel(deck, sizeMode);
734
804
 
805
+ const background = themeImage(
806
+ backgroundUrl
807
+ ? { image: backgroundUrl }
808
+ : resolveThemeBackground(themeMetadata, layout),
809
+ titleSlide ? "slide-background theme-cover-background" : "slide-background",
810
+ { decorative: true },
811
+ );
812
+ if (background) {
813
+ deck.classList.add("has-slide-background");
814
+ deck.appendChild(background);
815
+ }
735
816
  if (titleSlide) {
736
- const background = themeImage(
737
- themeMetadata?.cover?.background,
738
- "theme-cover-background",
739
- { decorative: true },
740
- );
741
- if (background) deck.appendChild(background);
742
817
  const logo = themeImage(themeMetadata?.cover?.logo, "theme-cover-logo");
743
818
  if (logo) deck.appendChild(logo);
744
819
  }
@@ -870,6 +945,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
870
945
  deck,
871
946
  bodyEl,
872
947
  theme,
948
+ backgroundImage,
873
949
  sizeMode,
874
950
  titleSlide,
875
951
  sectionSlide,
@@ -917,9 +993,15 @@ function renderSlide(markdown) {
917
993
  // that the slide is fully painted, and PDF export and the visual regression
918
994
  // suite rely on it. Revealing while an architecture icon under `assets/` is
919
995
  // still loading would capture a half-drawn slide.
920
- const images = waitForImages(slide.deck).then(() => {
921
- if (token === renderToken) scheduleLayoutRefresh();
922
- });
996
+ const images = waitForImages(slide.deck)
997
+ .then(() => {
998
+ if (token === renderToken) scheduleLayoutRefresh();
999
+ })
1000
+ .catch((error) => {
1001
+ if (token !== renderToken) return;
1002
+ console.error(error.message);
1003
+ showExportNotification("error", error.message);
1004
+ });
923
1005
  const mermaid = runMermaid(slide.bodyEl, slide.deck, token, false).finally(() => {
924
1006
  if (token === renderToken) scheduleLayoutRefresh();
925
1007
  });
@@ -961,7 +1043,14 @@ function waitForImages(root) {
961
1043
  }),
962
1044
  );
963
1045
  }
964
- return Promise.all(pending);
1046
+ return Promise.all(pending).then(() => {
1047
+ const failedBackground = [...root.querySelectorAll("img.slide-background")].find(
1048
+ (image) => !image.naturalWidth,
1049
+ );
1050
+ if (failedBackground) {
1051
+ throw new Error(`Could not load slide background: ${failedBackground.getAttribute("src")}`);
1052
+ }
1053
+ });
965
1054
  }
966
1055
 
967
1056
  async function reportOutputStatus(token, status, error = "", layout = null) {
@@ -2033,7 +2122,7 @@ function markMermaidNativeElements(svg, mappedElements, pathPrefix, sourceElemen
2033
2122
  source = sourceElements.get(ownerPath);
2034
2123
  }
2035
2124
  if (source && !source.hasAttribute("data-pptx-native")) {
2036
- const nativeKind = element.type === "shape" ? "shape" : element.type;
2125
+ const nativeKind = element.mermaid?.nativeMask || (element.type === "shape" ? "shape" : element.type);
2037
2126
  source.setAttribute("data-pptx-native", nativeKind);
2038
2127
  }
2039
2128
  if (!sourceElements && element.type === "connector") {
@@ -2098,10 +2187,17 @@ function collectMermaidObjects(element, deck, blockIndex) {
2098
2187
  mermaidElementForSourcePath(svg, sourcePath) ||
2099
2188
  mermaidFallbackElementForBounds(svg, deck, fallback) ||
2100
2189
  svg;
2101
- const sourceBounds = source.getBoundingClientRect();
2102
- const padding = ["path", "line", "polyline"].includes(source.localName)
2103
- ? Math.max(1, (fallback.width - sourceBounds.width) / 2, (fallback.height - sourceBounds.height) / 2)
2104
- : 0;
2190
+ const sourceBounds = fallbackBounds(source, deck, 0, true);
2191
+ const inferredPadding = Math.max(
2192
+ 0,
2193
+ (fallback.width - sourceBounds.width) / 2,
2194
+ (fallback.height - sourceBounds.height) / 2,
2195
+ );
2196
+ const geometryPadding = ["path", "line", "polyline"].includes(source.localName)
2197
+ ? Math.max(1, inferredPadding)
2198
+ : inferredPadding;
2199
+ const effectPadding = subtreeEffectPaintPadding(source);
2200
+ const padding = Math.max(geometryPadding, effectPadding);
2105
2201
  const captured = pptxFallback("mermaid", source, deck, fallback.reason, {
2106
2202
  captureElement: source,
2107
2203
  includeDescendants: true,
@@ -2113,10 +2209,14 @@ function collectMermaidObjects(element, deck, blockIndex) {
2113
2209
  path: fallback.path,
2114
2210
  sourcePath,
2115
2211
  reason: fallback.reason,
2116
- x: fallback.x,
2117
- y: fallback.y,
2118
- width: fallback.width,
2119
- height: fallback.height,
2212
+ ...(effectPadding
2213
+ ? {}
2214
+ : {
2215
+ x: fallback.x,
2216
+ y: fallback.y,
2217
+ width: fallback.width,
2218
+ height: fallback.height,
2219
+ }),
2120
2220
  zOrder: fallback.zOrder,
2121
2221
  ...(node?.id ? { id: node.id } : {}),
2122
2222
  ...(fallback.artwork === false ? { artwork: false } : {}),
@@ -2125,7 +2225,7 @@ function collectMermaidObjects(element, deck, blockIndex) {
2125
2225
  return { elements: mapped.elements, fallbacks };
2126
2226
  }
2127
2227
 
2128
- async function collectPptxSlide(slide, index) {
2228
+ async function collectPptxSlide(slide, index, options = {}) {
2129
2229
  const { deck } = slide;
2130
2230
  assignPptxPaintOrder(deck);
2131
2231
  const elements = [];
@@ -2232,6 +2332,12 @@ async function collectPptxSlide(slide, index) {
2232
2332
  for (const [blockIndex, element] of [...deck.querySelectorAll("pre.mermaid")].entries()) {
2233
2333
  const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
2234
2334
  if (covered) continue;
2335
+ if (options.mermaidImageFallback === true) {
2336
+ // The scroll container clips the diagram; SVG definitions have no painted bounds.
2337
+ // The capture pipeline trims transparent space inside this safe envelope.
2338
+ addFallback("mermaid", element, "mermaid-rendered-as-artwork");
2339
+ continue;
2340
+ }
2235
2341
  try {
2236
2342
  const mermaid = collectMermaidObjects(element, deck, blockIndex);
2237
2343
  elements.push(...mermaid.elements);
@@ -2496,7 +2602,7 @@ async function collectPptxSlide(slide, index) {
2496
2602
  for (const image of deck.querySelectorAll("img")) {
2497
2603
  if (
2498
2604
  image.closest(".architecture-diagram") ||
2499
- image.classList.contains("theme-cover-background") ||
2605
+ image.classList.contains("slide-background") ||
2500
2606
  image.classList.contains("theme-cover-logo") ||
2501
2607
  insideFallback(image)
2502
2608
  ) {
@@ -2597,20 +2703,20 @@ async function collectPptxSlide(slide, index) {
2597
2703
  };
2598
2704
  }
2599
2705
 
2600
- function createPptxLayoutTemplate(theme, layout) {
2706
+ function createPptxLayoutTemplate(theme, layout, id = `${theme}:${layout}`, backgroundImage) {
2601
2707
  const markdown = `---
2602
2708
  layout: ${layout}
2603
2709
  theme: ${theme}
2604
2710
  ---
2605
2711
  `;
2606
- const slide = createSlide(markdown, theme, true);
2712
+ const slide = createSlide(markdown, theme, true, { backgroundImage });
2607
2713
  if (layout === "backcover") {
2608
2714
  slide.deck
2609
2715
  .querySelectorAll(".theme-backcover-logo, .theme-backcover-copyright")
2610
2716
  .forEach((element) => element.remove());
2611
2717
  }
2612
2718
  slide.deck.classList.add("pptx-layout-template");
2613
- slide.deck.dataset.pptxLayoutId = `${theme}:${layout}`;
2719
+ slide.deck.dataset.pptxLayoutId = id;
2614
2720
  return slide;
2615
2721
  }
2616
2722
 
@@ -2620,6 +2726,7 @@ async function renderPptxDeck(
2620
2726
  customCss = "",
2621
2727
  themeMetadata = null,
2622
2728
  themeLocked = false,
2729
+ options = {},
2623
2730
  ) {
2624
2731
  deckTheme = normalizeTheme(theme);
2625
2732
  deckThemeLocked = Boolean(themeLocked);
@@ -2655,7 +2762,7 @@ async function renderPptxDeck(
2655
2762
 
2656
2763
  const pptxSlides = [];
2657
2764
  for (const [index, slide] of rendered.entries()) {
2658
- pptxSlides.push(await collectPptxSlide(slide, index));
2765
+ pptxSlides.push(await collectPptxSlide(slide, index, options));
2659
2766
  }
2660
2767
  const themes = [...new Set(rendered.map((slide) => slide.theme))];
2661
2768
  const layoutTemplates = themes.flatMap((slideTheme) =>
@@ -2666,6 +2773,26 @@ async function renderPptxDeck(
2666
2773
  slide: createPptxLayoutTemplate(slideTheme, layout),
2667
2774
  })),
2668
2775
  );
2776
+ // Per-slide overrides need their own layout artwork, not a background shared
2777
+ // with every slide of the same theme and layout.
2778
+ const backgroundLayouts = new Map();
2779
+ for (const [index, slide] of rendered.entries()) {
2780
+ if (!slide.backgroundImage) continue;
2781
+ const model = pptxSlides[index];
2782
+ const key = JSON.stringify([slide.theme, model.layout, slide.backgroundImage]);
2783
+ let id = backgroundLayouts.get(key);
2784
+ if (!id) {
2785
+ id = `${model.layoutId}:background-${backgroundLayouts.size + 1}`;
2786
+ backgroundLayouts.set(key, id);
2787
+ layoutTemplates.push({
2788
+ id,
2789
+ name: model.layout,
2790
+ theme: slide.theme,
2791
+ slide: createPptxLayoutTemplate(slide.theme, model.layout, id, slide.backgroundImage),
2792
+ });
2793
+ }
2794
+ model.layoutId = id;
2795
+ }
2669
2796
  stage.append(...layoutTemplates.map((layout) => layout.slide.deck));
2670
2797
  await waitForImages(stage);
2671
2798
  await afterLayout();
@@ -2683,7 +2810,7 @@ async function renderPptxDeck(
2683
2810
  masters: themes.map((slideTheme) => ({
2684
2811
  id: slideTheme,
2685
2812
  theme: slideTheme,
2686
- layoutIds: PPTX_LAYOUT_NAMES.map((layout) => `${slideTheme}:${layout}`),
2813
+ layoutIds: pptxLayouts.filter((layout) => layout.theme === slideTheme).map((layout) => layout.id),
2687
2814
  })),
2688
2815
  layouts: pptxLayouts,
2689
2816
  slides: pptxSlides,
@@ -2721,6 +2848,7 @@ async function initPptx(params) {
2721
2848
  data.customThemeCss,
2722
2849
  data.customThemeMeta,
2723
2850
  data.themeLocked,
2851
+ { mermaidImageFallback: data.mermaidImageFallback === true },
2724
2852
  );
2725
2853
  await reportOutputStatus(token, "ready", "", output.layout);
2726
2854
  } catch (error) {
@@ -3681,9 +3809,45 @@ function showExportNotification(state, message, path = "") {
3681
3809
  resumeExportNotification();
3682
3810
  }
3683
3811
 
3684
- async function exportFromCanvas(format) {
3812
+ let pptxOptionsPending = false;
3813
+
3814
+ async function requestPptxExport() {
3815
+ const dialog = document.getElementById("pptxExportDialog");
3816
+ if (exportPending || pptxOptionsPending || dialog.open || !pptxExportAvailable) return;
3817
+ pptxOptionsPending = true;
3818
+ let hasMermaid;
3819
+ try {
3820
+ const response = await fetch("./deck", { cache: "no-store" });
3821
+ if (!response.ok) throw new Error(`Could not load the export deck (${response.status}).`);
3822
+ const data = await response.json();
3823
+ if (!Array.isArray(data?.slides) || !data.slides.every((slide) => typeof slide === "string")) {
3824
+ throw new Error("The export deck is invalid.");
3825
+ }
3826
+ const slides = [...data.slides];
3827
+ if (navMode === "adhoc") slides[Math.max(0, Math.min(navIndex, slides.length - 1))] = lastMarkdown;
3828
+ hasMermaid = slides.some((markdown) => {
3829
+ const { body } = splitFrontMatter(markdown);
3830
+ const content = document.createElement("div");
3831
+ content.innerHTML = window.DOMPurify.sanitize(window.marked.parse(stripSpeakerNotes(body)));
3832
+ return codeBlocksForLanguage(content, "mermaid").length > 0 || Boolean(content.querySelector(".mermaid"));
3833
+ });
3834
+ } catch (error) {
3835
+ showExportNotification("error", `Could not save PowerPoint. ${error.message}`);
3836
+ return;
3837
+ } finally {
3838
+ pptxOptionsPending = false;
3839
+ }
3840
+ if (hasMermaid) {
3841
+ document.getElementById("pptxExportForm").reset();
3842
+ dialog.showModal();
3843
+ } else {
3844
+ await exportFromCanvas("pptx");
3845
+ }
3846
+ }
3847
+
3848
+ async function exportFromCanvas(format, { mermaidImageFallback = false } = {}) {
3685
3849
  const isPdf = format === "pdf";
3686
- if (exportPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
3850
+ if (exportPending || pptxOptionsPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
3687
3851
  exportPending = true;
3688
3852
  const label = isPdf ? "PDF" : "PowerPoint";
3689
3853
  const pdfButton = document.getElementById("navExport");
@@ -3700,7 +3864,11 @@ async function exportFromCanvas(format) {
3700
3864
  try {
3701
3865
  const response = await fetch(isPdf ? "./export" : "./export-pptx", {
3702
3866
  method: "POST",
3703
- headers: { Accept: "application/json" },
3867
+ headers: {
3868
+ Accept: "application/json",
3869
+ ...(isPdf ? {} : { "Content-Type": "application/json" }),
3870
+ },
3871
+ ...(isPdf ? {} : { body: JSON.stringify({ mermaidImageFallback }) }),
3704
3872
  cache: "no-store",
3705
3873
  });
3706
3874
  const data = await response.json();
@@ -4175,7 +4343,7 @@ function wireControls() {
4175
4343
  bind("navPresenterView", openPresenterView, { closeMore: true });
4176
4344
  bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
4177
4345
  bind("navExport", () => exportFromCanvas("pdf"), { closeMore: true });
4178
- bind("navExportPptx", () => exportFromCanvas("pptx"), { closeMore: true });
4346
+ bind("navExportPptx", requestPptxExport, { closeMore: true });
4179
4347
  bind("navImport", toggleImportPicker, { closeMore: true });
4180
4348
  bind("navSourceMode", toggleSourceMode, { closeMore: true });
4181
4349
  bind("overviewClose", closeOverview);
@@ -4187,6 +4355,29 @@ function wireControls() {
4187
4355
  bind("presenterToggleButton", togglePresenterWindow);
4188
4356
  bind("presenterReturnButton", closePresenterView);
4189
4357
 
4358
+ const pptxDialog = document.getElementById("pptxExportDialog");
4359
+ pptxDialog.addEventListener("keydown", (event) => {
4360
+ if (event.key !== "Tab") return;
4361
+ const controls = [...pptxDialog.querySelectorAll("input:checked, button:not(:disabled)")];
4362
+ const first = controls[0];
4363
+ const last = controls.at(-1);
4364
+ if (event.shiftKey && document.activeElement === first) {
4365
+ event.preventDefault();
4366
+ last.focus();
4367
+ } else if (!event.shiftKey && document.activeElement === last) {
4368
+ event.preventDefault();
4369
+ first.focus();
4370
+ }
4371
+ });
4372
+ document.getElementById("pptxExportCancel").addEventListener("click", () => pptxDialog.close());
4373
+ pptxDialog.addEventListener("close", () => document.getElementById("navMore").focus());
4374
+ document.getElementById("pptxExportForm").addEventListener("submit", (event) => {
4375
+ event.preventDefault();
4376
+ const mermaidImageFallback = new FormData(event.currentTarget).get("mermaidOutput") === "images";
4377
+ pptxDialog.close();
4378
+ exportFromCanvas("pptx", { mermaidImageFallback });
4379
+ });
4380
+
4190
4381
  const exportNotification = document.getElementById("exportNotification");
4191
4382
  document.getElementById("exportNotificationClose").addEventListener("click", dismissExportNotification);
4192
4383
  exportNotification.addEventListener("pointerenter", pauseExportNotification);
@@ -4242,6 +4433,7 @@ function wireControls() {
4242
4433
 
4243
4434
  document.addEventListener("keydown", (e) => {
4244
4435
  if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) return;
4436
+ if (pptxDialog.open) return;
4245
4437
  const t = e.target;
4246
4438
  // Keep Esc active while an input has focus; otherwise filtering in the import
4247
4439
  // dialog could leave the user unable to close it.