@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.
- package/README.md +2 -1
- package/package.json +1 -1
- package/shared/README.md +40 -4
- package/shared/architecture-editor/editor.css +9 -5
- package/shared/architecture-editor/editor.js +440 -75
- package/shared/architecture-editor/index.html +2 -2
- package/shared/docs/custom-theme-authoring.md +61 -4
- package/shared/markdown-deck.mjs +9 -5
- package/shared/renderer/architecture-document.mjs +169 -10
- package/shared/renderer/index.html +24 -1
- package/shared/renderer/mermaid-scene.mjs +6725 -197
- package/shared/renderer/renderer.js +245 -53
- package/shared/renderer/scene-graph.mjs +83 -13
- package/shared/renderer/scene-pptx.mjs +154 -1
- package/shared/renderer/scene-svg.mjs +227 -11
- package/shared/renderer/slide-background.mjs +22 -0
- package/shared/renderer/slides.css +32 -6
- package/shared/renderer/theme.mjs +328 -12
- package/shared/runtime/browser.mjs +75 -5
- package/shared/runtime/deck-session.mjs +12 -17
- package/shared/runtime/output-paths.mjs +7 -0
- package/shared/runtime/output.mjs +4 -2
- package/shared/runtime/pptx-package.mjs +103 -22
- package/shared/runtime/presentation-server.mjs +44 -2
- package/shared/runtime/slide-backgrounds.mjs +44 -0
- package/shared/schema/theme-metadata-v1.schema.json +25 -0
- package/shared/schema/theme-v1.json +3 -3
- package/src/cli.mjs +9 -2
- package/src/commands/export.mjs +2 -0
|
@@ -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
|
|
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 =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
627
|
-
|
|
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:
|
|
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:
|
|
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
|
-
:
|
|
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(
|
|
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)
|
|
921
|
-
|
|
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
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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("
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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: {
|
|
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",
|
|
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.
|