@markdstage/markdstage 3.3.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 +25 -13
- package/package.json +1 -1
- package/shared/README.md +47 -10
- 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 +260 -55
- 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 +35 -9
- 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 +410 -95
- 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 +101 -21
- package/src/commands/export.mjs +2 -0
- package/src/commands/present.mjs +133 -143
- package/src/deck.mjs +4 -0
- package/src/skills.mjs +12 -8
|
@@ -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,
|
|
@@ -426,13 +429,14 @@ function collectDeckLayout(rendered) {
|
|
|
426
429
|
function updateFixedPreviewWarning() {
|
|
427
430
|
const warning = document.getElementById("layoutWarning");
|
|
428
431
|
const button = document.getElementById("navFixedPreview");
|
|
429
|
-
|
|
432
|
+
const empty = document.body.classList.contains("markdstage-empty");
|
|
433
|
+
if (!fixedPreviewMode || !layoutTarget || empty) {
|
|
430
434
|
document.body.classList.remove("fixed-preview-overflow");
|
|
431
435
|
if (warning) {
|
|
432
436
|
warning.hidden = true;
|
|
433
437
|
warning.textContent = "";
|
|
434
438
|
}
|
|
435
|
-
if (button) button.dataset.state = fixedPreviewMode ? "active" : "";
|
|
439
|
+
if (button) button.dataset.state = fixedPreviewMode && !empty ? "active" : "";
|
|
436
440
|
syncMoreControls();
|
|
437
441
|
return;
|
|
438
442
|
}
|
|
@@ -582,13 +586,19 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
582
586
|
return Promise.resolve();
|
|
583
587
|
}
|
|
584
588
|
try {
|
|
585
|
-
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);
|
|
586
595
|
const serializedThemeVariables = JSON.stringify(themeVariables);
|
|
587
596
|
if (serializedThemeVariables !== lastMermaidThemeVariables) {
|
|
588
597
|
window.mermaid.initialize({
|
|
589
598
|
startOnLoad: false,
|
|
590
599
|
theme: "base",
|
|
591
600
|
themeVariables,
|
|
601
|
+
c4: c4ThemeVariables,
|
|
592
602
|
securityLevel: "strict",
|
|
593
603
|
});
|
|
594
604
|
lastMermaidThemeVariables = serializedThemeVariables;
|
|
@@ -600,11 +610,60 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
600
610
|
const source = host.querySelector("svg");
|
|
601
611
|
if (!source || source.hasAttribute("data-scene-backend")) continue;
|
|
602
612
|
try {
|
|
613
|
+
repairC4ThemeDefaults(source, sources[index], themeVariables);
|
|
603
614
|
renderMermaidScene(source, deckEl, index);
|
|
604
615
|
} catch (e) {
|
|
605
616
|
console.error("Mermaid scene render failed", e);
|
|
606
617
|
}
|
|
607
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
|
+
}
|
|
608
667
|
})
|
|
609
668
|
.finally(reveal);
|
|
610
669
|
} catch (e) {
|
|
@@ -614,26 +673,28 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
614
673
|
}
|
|
615
674
|
}
|
|
616
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
|
+
|
|
617
686
|
function renderMermaidScene(svg, deck, blockIndex) {
|
|
618
|
-
const result =
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
687
|
+
const result = withoutMermaidLoadingVeil(() =>
|
|
688
|
+
mermaidSvgToScene(svg, {
|
|
689
|
+
path: `mermaid[${blockIndex}]`,
|
|
690
|
+
deck,
|
|
691
|
+
includeSourceElements: true,
|
|
692
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
693
|
+
}),
|
|
694
|
+
);
|
|
624
695
|
let { scene } = result;
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
return {
|
|
628
|
-
getPropertyValue(property) {
|
|
629
|
-
// The loading veil is inherited by every SVG descendant, not diagram style.
|
|
630
|
-
if (property === "visibility" && document.body.classList.contains("mermaid-loading")) {
|
|
631
|
-
return element.style?.visibility || element.getAttribute("visibility") || "";
|
|
632
|
-
}
|
|
633
|
-
return style.getPropertyValue(property);
|
|
634
|
-
},
|
|
635
|
-
};
|
|
636
|
-
};
|
|
696
|
+
const capture = (element, options) =>
|
|
697
|
+
withoutMermaidLoadingVeil(() => captureSvgTree(element, options));
|
|
637
698
|
const slots = new Map();
|
|
638
699
|
try {
|
|
639
700
|
for (const [index, node] of scene.nodes.entries()) {
|
|
@@ -656,7 +717,7 @@ function renderMermaidScene(svg, deck, blockIndex) {
|
|
|
656
717
|
node.meta = { ...node.meta, svgOwner: slots.get(source) };
|
|
657
718
|
continue;
|
|
658
719
|
}
|
|
659
|
-
node.meta = { ...node.meta, svg:
|
|
720
|
+
node.meta = { ...node.meta, svg: capture(source) };
|
|
660
721
|
slots.set(source, index);
|
|
661
722
|
}
|
|
662
723
|
for (const [source, index] of slots) {
|
|
@@ -675,14 +736,14 @@ function renderMermaidScene(svg, deck, blockIndex) {
|
|
|
675
736
|
kind: "fallback", sourcePath: "svg", z: 0,
|
|
676
737
|
bounds: { x: 0, y: 0, width: scene.width, height: scene.height },
|
|
677
738
|
capability: { pptx: "fallback", reason }, reason,
|
|
678
|
-
meta: { svg:
|
|
739
|
+
meta: { svg: capture(svg) },
|
|
679
740
|
}] };
|
|
680
741
|
slots.clear();
|
|
681
742
|
slots.set(svg, 0);
|
|
682
743
|
}
|
|
683
744
|
const template = slots.has(svg)
|
|
684
745
|
? { sceneNode: slots.get(svg) }
|
|
685
|
-
:
|
|
746
|
+
: capture(svg, { slots });
|
|
686
747
|
scene.meta = { ...scene.meta, svgRoot: template };
|
|
687
748
|
svg.replaceWith(sceneToSvg(scene, { document, template }));
|
|
688
749
|
}
|
|
@@ -697,7 +758,12 @@ function moveLeadingSlideTitle(header, bodyEl, specialLayout) {
|
|
|
697
758
|
return title;
|
|
698
759
|
}
|
|
699
760
|
|
|
700
|
-
function createSlide(
|
|
761
|
+
function createSlide(
|
|
762
|
+
markdown,
|
|
763
|
+
fallbackTheme,
|
|
764
|
+
themeLocked = deckThemeLocked,
|
|
765
|
+
{ backgroundImage: backgroundOverride } = {},
|
|
766
|
+
) {
|
|
701
767
|
const placeholder = !nonEmpty(markdown);
|
|
702
768
|
const md = placeholder ? PLACEHOLDER : markdown;
|
|
703
769
|
const { meta, body: rawBody } = splitFrontMatter(md);
|
|
@@ -720,6 +786,11 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
720
786
|
// as well as <html> so print mode can render differently themed pages together.
|
|
721
787
|
const theme = normalizeTheme(themeLocked ? fallbackTheme : meta.theme || fallbackTheme);
|
|
722
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
|
+
: "";
|
|
723
794
|
|
|
724
795
|
const deck = document.createElement("div");
|
|
725
796
|
deck.className = "deck";
|
|
@@ -731,13 +802,18 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
731
802
|
if (placeholder) deck.classList.add("markdstage-placeholder");
|
|
732
803
|
if (sizeMode !== "auto") setSizeLevel(deck, sizeMode);
|
|
733
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
|
+
}
|
|
734
816
|
if (titleSlide) {
|
|
735
|
-
const background = themeImage(
|
|
736
|
-
themeMetadata?.cover?.background,
|
|
737
|
-
"theme-cover-background",
|
|
738
|
-
{ decorative: true },
|
|
739
|
-
);
|
|
740
|
-
if (background) deck.appendChild(background);
|
|
741
817
|
const logo = themeImage(themeMetadata?.cover?.logo, "theme-cover-logo");
|
|
742
818
|
if (logo) deck.appendChild(logo);
|
|
743
819
|
}
|
|
@@ -869,6 +945,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
869
945
|
deck,
|
|
870
946
|
bodyEl,
|
|
871
947
|
theme,
|
|
948
|
+
backgroundImage,
|
|
872
949
|
sizeMode,
|
|
873
950
|
titleSlide,
|
|
874
951
|
sectionSlide,
|
|
@@ -916,9 +993,15 @@ function renderSlide(markdown) {
|
|
|
916
993
|
// that the slide is fully painted, and PDF export and the visual regression
|
|
917
994
|
// suite rely on it. Revealing while an architecture icon under `assets/` is
|
|
918
995
|
// still loading would capture a half-drawn slide.
|
|
919
|
-
const images = waitForImages(slide.deck)
|
|
920
|
-
|
|
921
|
-
|
|
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
|
+
});
|
|
922
1005
|
const mermaid = runMermaid(slide.bodyEl, slide.deck, token, false).finally(() => {
|
|
923
1006
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
924
1007
|
});
|
|
@@ -960,7 +1043,14 @@ function waitForImages(root) {
|
|
|
960
1043
|
}),
|
|
961
1044
|
);
|
|
962
1045
|
}
|
|
963
|
-
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
|
+
});
|
|
964
1054
|
}
|
|
965
1055
|
|
|
966
1056
|
async function reportOutputStatus(token, status, error = "", layout = null) {
|
|
@@ -2032,7 +2122,7 @@ function markMermaidNativeElements(svg, mappedElements, pathPrefix, sourceElemen
|
|
|
2032
2122
|
source = sourceElements.get(ownerPath);
|
|
2033
2123
|
}
|
|
2034
2124
|
if (source && !source.hasAttribute("data-pptx-native")) {
|
|
2035
|
-
const nativeKind = element.type === "shape" ? "shape" : element.type;
|
|
2125
|
+
const nativeKind = element.mermaid?.nativeMask || (element.type === "shape" ? "shape" : element.type);
|
|
2036
2126
|
source.setAttribute("data-pptx-native", nativeKind);
|
|
2037
2127
|
}
|
|
2038
2128
|
if (!sourceElements && element.type === "connector") {
|
|
@@ -2097,10 +2187,17 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2097
2187
|
mermaidElementForSourcePath(svg, sourcePath) ||
|
|
2098
2188
|
mermaidFallbackElementForBounds(svg, deck, fallback) ||
|
|
2099
2189
|
svg;
|
|
2100
|
-
const sourceBounds = source
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
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);
|
|
2104
2201
|
const captured = pptxFallback("mermaid", source, deck, fallback.reason, {
|
|
2105
2202
|
captureElement: source,
|
|
2106
2203
|
includeDescendants: true,
|
|
@@ -2112,10 +2209,14 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2112
2209
|
path: fallback.path,
|
|
2113
2210
|
sourcePath,
|
|
2114
2211
|
reason: fallback.reason,
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2212
|
+
...(effectPadding
|
|
2213
|
+
? {}
|
|
2214
|
+
: {
|
|
2215
|
+
x: fallback.x,
|
|
2216
|
+
y: fallback.y,
|
|
2217
|
+
width: fallback.width,
|
|
2218
|
+
height: fallback.height,
|
|
2219
|
+
}),
|
|
2119
2220
|
zOrder: fallback.zOrder,
|
|
2120
2221
|
...(node?.id ? { id: node.id } : {}),
|
|
2121
2222
|
...(fallback.artwork === false ? { artwork: false } : {}),
|
|
@@ -2124,7 +2225,7 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2124
2225
|
return { elements: mapped.elements, fallbacks };
|
|
2125
2226
|
}
|
|
2126
2227
|
|
|
2127
|
-
async function collectPptxSlide(slide, index) {
|
|
2228
|
+
async function collectPptxSlide(slide, index, options = {}) {
|
|
2128
2229
|
const { deck } = slide;
|
|
2129
2230
|
assignPptxPaintOrder(deck);
|
|
2130
2231
|
const elements = [];
|
|
@@ -2231,6 +2332,12 @@ async function collectPptxSlide(slide, index) {
|
|
|
2231
2332
|
for (const [blockIndex, element] of [...deck.querySelectorAll("pre.mermaid")].entries()) {
|
|
2232
2333
|
const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
2233
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
|
+
}
|
|
2234
2341
|
try {
|
|
2235
2342
|
const mermaid = collectMermaidObjects(element, deck, blockIndex);
|
|
2236
2343
|
elements.push(...mermaid.elements);
|
|
@@ -2495,7 +2602,7 @@ async function collectPptxSlide(slide, index) {
|
|
|
2495
2602
|
for (const image of deck.querySelectorAll("img")) {
|
|
2496
2603
|
if (
|
|
2497
2604
|
image.closest(".architecture-diagram") ||
|
|
2498
|
-
image.classList.contains("
|
|
2605
|
+
image.classList.contains("slide-background") ||
|
|
2499
2606
|
image.classList.contains("theme-cover-logo") ||
|
|
2500
2607
|
insideFallback(image)
|
|
2501
2608
|
) {
|
|
@@ -2596,20 +2703,20 @@ async function collectPptxSlide(slide, index) {
|
|
|
2596
2703
|
};
|
|
2597
2704
|
}
|
|
2598
2705
|
|
|
2599
|
-
function createPptxLayoutTemplate(theme, layout) {
|
|
2706
|
+
function createPptxLayoutTemplate(theme, layout, id = `${theme}:${layout}`, backgroundImage) {
|
|
2600
2707
|
const markdown = `---
|
|
2601
2708
|
layout: ${layout}
|
|
2602
2709
|
theme: ${theme}
|
|
2603
2710
|
---
|
|
2604
2711
|
`;
|
|
2605
|
-
const slide = createSlide(markdown, theme, true);
|
|
2712
|
+
const slide = createSlide(markdown, theme, true, { backgroundImage });
|
|
2606
2713
|
if (layout === "backcover") {
|
|
2607
2714
|
slide.deck
|
|
2608
2715
|
.querySelectorAll(".theme-backcover-logo, .theme-backcover-copyright")
|
|
2609
2716
|
.forEach((element) => element.remove());
|
|
2610
2717
|
}
|
|
2611
2718
|
slide.deck.classList.add("pptx-layout-template");
|
|
2612
|
-
slide.deck.dataset.pptxLayoutId =
|
|
2719
|
+
slide.deck.dataset.pptxLayoutId = id;
|
|
2613
2720
|
return slide;
|
|
2614
2721
|
}
|
|
2615
2722
|
|
|
@@ -2619,6 +2726,7 @@ async function renderPptxDeck(
|
|
|
2619
2726
|
customCss = "",
|
|
2620
2727
|
themeMetadata = null,
|
|
2621
2728
|
themeLocked = false,
|
|
2729
|
+
options = {},
|
|
2622
2730
|
) {
|
|
2623
2731
|
deckTheme = normalizeTheme(theme);
|
|
2624
2732
|
deckThemeLocked = Boolean(themeLocked);
|
|
@@ -2654,7 +2762,7 @@ async function renderPptxDeck(
|
|
|
2654
2762
|
|
|
2655
2763
|
const pptxSlides = [];
|
|
2656
2764
|
for (const [index, slide] of rendered.entries()) {
|
|
2657
|
-
pptxSlides.push(await collectPptxSlide(slide, index));
|
|
2765
|
+
pptxSlides.push(await collectPptxSlide(slide, index, options));
|
|
2658
2766
|
}
|
|
2659
2767
|
const themes = [...new Set(rendered.map((slide) => slide.theme))];
|
|
2660
2768
|
const layoutTemplates = themes.flatMap((slideTheme) =>
|
|
@@ -2665,6 +2773,26 @@ async function renderPptxDeck(
|
|
|
2665
2773
|
slide: createPptxLayoutTemplate(slideTheme, layout),
|
|
2666
2774
|
})),
|
|
2667
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
|
+
}
|
|
2668
2796
|
stage.append(...layoutTemplates.map((layout) => layout.slide.deck));
|
|
2669
2797
|
await waitForImages(stage);
|
|
2670
2798
|
await afterLayout();
|
|
@@ -2682,7 +2810,7 @@ async function renderPptxDeck(
|
|
|
2682
2810
|
masters: themes.map((slideTheme) => ({
|
|
2683
2811
|
id: slideTheme,
|
|
2684
2812
|
theme: slideTheme,
|
|
2685
|
-
layoutIds:
|
|
2813
|
+
layoutIds: pptxLayouts.filter((layout) => layout.theme === slideTheme).map((layout) => layout.id),
|
|
2686
2814
|
})),
|
|
2687
2815
|
layouts: pptxLayouts,
|
|
2688
2816
|
slides: pptxSlides,
|
|
@@ -2720,6 +2848,7 @@ async function initPptx(params) {
|
|
|
2720
2848
|
data.customThemeCss,
|
|
2721
2849
|
data.customThemeMeta,
|
|
2722
2850
|
data.themeLocked,
|
|
2851
|
+
{ mermaidImageFallback: data.mermaidImageFallback === true },
|
|
2723
2852
|
);
|
|
2724
2853
|
await reportOutputStatus(token, "ready", "", output.layout);
|
|
2725
2854
|
} catch (error) {
|
|
@@ -3020,6 +3149,7 @@ async function fetchDeck() {
|
|
|
3020
3149
|
function setArchitectureEditMode(enabled) {
|
|
3021
3150
|
const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
|
|
3022
3151
|
if (next === architectureEditMode) return false;
|
|
3152
|
+
if (next && fixedPreviewMode) setFixedPreviewMode(false);
|
|
3023
3153
|
architectureEditMode = next;
|
|
3024
3154
|
document.body.classList.toggle("architecture-edit-mode", next);
|
|
3025
3155
|
updateArchitectureEditButton(next);
|
|
@@ -3679,9 +3809,45 @@ function showExportNotification(state, message, path = "") {
|
|
|
3679
3809
|
resumeExportNotification();
|
|
3680
3810
|
}
|
|
3681
3811
|
|
|
3682
|
-
|
|
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 } = {}) {
|
|
3683
3849
|
const isPdf = format === "pdf";
|
|
3684
|
-
if (exportPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3850
|
+
if (exportPending || pptxOptionsPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3685
3851
|
exportPending = true;
|
|
3686
3852
|
const label = isPdf ? "PDF" : "PowerPoint";
|
|
3687
3853
|
const pdfButton = document.getElementById("navExport");
|
|
@@ -3698,7 +3864,11 @@ async function exportFromCanvas(format) {
|
|
|
3698
3864
|
try {
|
|
3699
3865
|
const response = await fetch(isPdf ? "./export" : "./export-pptx", {
|
|
3700
3866
|
method: "POST",
|
|
3701
|
-
headers: {
|
|
3867
|
+
headers: {
|
|
3868
|
+
Accept: "application/json",
|
|
3869
|
+
...(isPdf ? {} : { "Content-Type": "application/json" }),
|
|
3870
|
+
},
|
|
3871
|
+
...(isPdf ? {} : { body: JSON.stringify({ mermaidImageFallback }) }),
|
|
3702
3872
|
cache: "no-store",
|
|
3703
3873
|
});
|
|
3704
3874
|
const data = await response.json();
|
|
@@ -4173,7 +4343,7 @@ function wireControls() {
|
|
|
4173
4343
|
bind("navPresenterView", openPresenterView, { closeMore: true });
|
|
4174
4344
|
bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
|
|
4175
4345
|
bind("navExport", () => exportFromCanvas("pdf"), { closeMore: true });
|
|
4176
|
-
bind("navExportPptx",
|
|
4346
|
+
bind("navExportPptx", requestPptxExport, { closeMore: true });
|
|
4177
4347
|
bind("navImport", toggleImportPicker, { closeMore: true });
|
|
4178
4348
|
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
4179
4349
|
bind("overviewClose", closeOverview);
|
|
@@ -4185,6 +4355,29 @@ function wireControls() {
|
|
|
4185
4355
|
bind("presenterToggleButton", togglePresenterWindow);
|
|
4186
4356
|
bind("presenterReturnButton", closePresenterView);
|
|
4187
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
|
+
|
|
4188
4381
|
const exportNotification = document.getElementById("exportNotification");
|
|
4189
4382
|
document.getElementById("exportNotificationClose").addEventListener("click", dismissExportNotification);
|
|
4190
4383
|
exportNotification.addEventListener("pointerenter", pauseExportNotification);
|
|
@@ -4240,6 +4433,7 @@ function wireControls() {
|
|
|
4240
4433
|
|
|
4241
4434
|
document.addEventListener("keydown", (e) => {
|
|
4242
4435
|
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
4436
|
+
if (pptxDialog.open) return;
|
|
4243
4437
|
const t = e.target;
|
|
4244
4438
|
// Keep Esc active while an input has focus; otherwise filtering in the import
|
|
4245
4439
|
// dialog could leave the user unable to close it.
|
|
@@ -4352,6 +4546,17 @@ function init() {
|
|
|
4352
4546
|
requestArchitectureEditMode(true);
|
|
4353
4547
|
}
|
|
4354
4548
|
|
|
4549
|
+
// Canvas and CLI preview start on the fixed 16:9 output surface. Presenter
|
|
4550
|
+
// views keep their purpose-built layouts, and the control still lets users
|
|
4551
|
+
// switch back to the responsive canvas layout.
|
|
4552
|
+
if (
|
|
4553
|
+
!presenterMode &&
|
|
4554
|
+
!presenterViewRequested &&
|
|
4555
|
+
params.get("responsive") !== "1"
|
|
4556
|
+
) {
|
|
4557
|
+
setFixedPreviewMode(true);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4355
4560
|
updateArchitectureEditButton();
|
|
4356
4561
|
if (!previewMode) wireControls();
|
|
4357
4562
|
else if (navigationEnabled) {
|