@markdstage/markdstage 3.4.0 → 3.8.1
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 +26 -3
- package/shared/renderer/mermaid-scene.mjs +6725 -197
- package/shared/renderer/renderer.js +421 -102
- 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/slide-viewport.mjs +48 -0
- package/shared/renderer/slides.css +41 -10
- 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,
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
stripSpeakerNotes,
|
|
18
21
|
} from "./speaker-notes.mjs";
|
|
19
22
|
import { splitImportPath } from "./import-path.mjs";
|
|
23
|
+
import { createSlideViewport, OUTPUT_WIDTH, OUTPUT_HEIGHT } from "./slide-viewport.mjs";
|
|
20
24
|
|
|
21
25
|
// Client-side slide renderer for the MarkdStage canvas.
|
|
22
26
|
//
|
|
@@ -102,6 +106,11 @@ let previewMode = false;
|
|
|
102
106
|
let previewOffset = 0;
|
|
103
107
|
let navigationEnabled = true;
|
|
104
108
|
let fixedPreviewMode = false;
|
|
109
|
+
let surfaceMode = false;
|
|
110
|
+
let outputViewport = null;
|
|
111
|
+
let currentViewport = null;
|
|
112
|
+
let nextViewport = null;
|
|
113
|
+
let surfaceDiagnostic = null;
|
|
105
114
|
let moreControlsOpen = false;
|
|
106
115
|
// Markdown for the most recently rendered slide, retained for editing-mode rerenders.
|
|
107
116
|
let lastMarkdown = "";
|
|
@@ -119,8 +128,6 @@ let layoutFrame = 0;
|
|
|
119
128
|
// past clientHeight, which is invisible but still enough for `overflow:auto` to
|
|
120
129
|
// draw a scrollbar.
|
|
121
130
|
const SCROLL_EPSILON = 2;
|
|
122
|
-
const OUTPUT_WIDTH = 1280;
|
|
123
|
-
const OUTPUT_HEIGHT = 720;
|
|
124
131
|
const PPTX_LAYOUT_NAMES = ["title", "default", "center", "section", "backcover"];
|
|
125
132
|
const LAYOUT_HINT_LIMIT = 5;
|
|
126
133
|
|
|
@@ -265,11 +272,8 @@ function elementPath(element, root) {
|
|
|
265
272
|
return parts.join(" > ");
|
|
266
273
|
}
|
|
267
274
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// layout space. Every rect-derived delta is divided by this factor before the two kinds of
|
|
271
|
-
// measurement are combined, keeping the preview, PDF, and PNG diagnostics in one coordinate
|
|
272
|
-
// system.
|
|
275
|
+
// Normalize any content-authored transform before combining rect and scroll metrics.
|
|
276
|
+
// Display scaling happens outside the fixed browsing context and is not measured here.
|
|
273
277
|
function layoutScale(deck) {
|
|
274
278
|
const width = deck.offsetWidth;
|
|
275
279
|
if (!width) return 1;
|
|
@@ -427,7 +431,9 @@ function updateFixedPreviewWarning() {
|
|
|
427
431
|
const warning = document.getElementById("layoutWarning");
|
|
428
432
|
const button = document.getElementById("navFixedPreview");
|
|
429
433
|
const empty = document.body.classList.contains("markdstage-empty");
|
|
430
|
-
|
|
434
|
+
const diagnostic = fixedPreviewMode && !presenterMode &&
|
|
435
|
+
(surfaceDiagnostic || (layoutTarget && collectSlideLayout(layoutTarget, navIndex)));
|
|
436
|
+
if (!fixedPreviewMode || !diagnostic || empty || presenterMode) {
|
|
431
437
|
document.body.classList.remove("fixed-preview-overflow");
|
|
432
438
|
if (warning) {
|
|
433
439
|
warning.hidden = true;
|
|
@@ -438,7 +444,6 @@ function updateFixedPreviewWarning() {
|
|
|
438
444
|
return;
|
|
439
445
|
}
|
|
440
446
|
|
|
441
|
-
const diagnostic = collectSlideLayout(layoutTarget, navIndex);
|
|
442
447
|
document.body.classList.toggle("fixed-preview-overflow", diagnostic.pdfClipped);
|
|
443
448
|
if (button) button.dataset.state = diagnostic.pdfClipped ? "error" : "active";
|
|
444
449
|
syncMoreControls();
|
|
@@ -459,20 +464,17 @@ function updateFixedPreviewWarning() {
|
|
|
459
464
|
warning.hidden = false;
|
|
460
465
|
}
|
|
461
466
|
|
|
462
|
-
function updateFixedPreviewScale() {
|
|
463
|
-
if (!fixedPreviewMode) return;
|
|
464
|
-
const availableWidth = Math.max(1, window.innerWidth - 24);
|
|
465
|
-
const availableHeight = Math.max(1, window.innerHeight - 88);
|
|
466
|
-
const scale = Math.min(availableWidth / OUTPUT_WIDTH, availableHeight / OUTPUT_HEIGHT, 1);
|
|
467
|
-
document.body.style.setProperty("--fixed-preview-scale", String(scale));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
467
|
function refreshLayout() {
|
|
471
468
|
const target = layoutTarget;
|
|
472
469
|
if (!target || !target.deck.isConnected) return;
|
|
473
470
|
if (target.autoSize) applyAutoSize(target.deck, target.bodyEl);
|
|
474
471
|
updateBodyScroll(target.bodyEl);
|
|
475
472
|
updateFixedPreviewWarning();
|
|
473
|
+
if (surfaceMode && !document.body.classList.contains("mermaid-loading")) {
|
|
474
|
+
window.frameElement?.dispatchEvent(new CustomEvent("slide-layout", {
|
|
475
|
+
detail: collectSlideLayout(target, navIndex),
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
476
478
|
}
|
|
477
479
|
|
|
478
480
|
// Coalesce the (re)layout into one frame: several triggers — render, resize,
|
|
@@ -583,13 +585,19 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
583
585
|
return Promise.resolve();
|
|
584
586
|
}
|
|
585
587
|
try {
|
|
586
|
-
const
|
|
588
|
+
const sources = [...nodes].map((node) => node.textContent || "");
|
|
589
|
+
const themeVariables = mermaidThemeVariables(
|
|
590
|
+
getComputedStyle(deckEl),
|
|
591
|
+
(value) => resolveModelColor(value, deckEl),
|
|
592
|
+
);
|
|
593
|
+
const c4ThemeVariables = mermaidC4ThemeVariables(themeVariables);
|
|
587
594
|
const serializedThemeVariables = JSON.stringify(themeVariables);
|
|
588
595
|
if (serializedThemeVariables !== lastMermaidThemeVariables) {
|
|
589
596
|
window.mermaid.initialize({
|
|
590
597
|
startOnLoad: false,
|
|
591
598
|
theme: "base",
|
|
592
599
|
themeVariables,
|
|
600
|
+
c4: c4ThemeVariables,
|
|
593
601
|
securityLevel: "strict",
|
|
594
602
|
});
|
|
595
603
|
lastMermaidThemeVariables = serializedThemeVariables;
|
|
@@ -601,11 +609,60 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
601
609
|
const source = host.querySelector("svg");
|
|
602
610
|
if (!source || source.hasAttribute("data-scene-backend")) continue;
|
|
603
611
|
try {
|
|
612
|
+
repairC4ThemeDefaults(source, sources[index], themeVariables);
|
|
604
613
|
renderMermaidScene(source, deckEl, index);
|
|
605
614
|
} catch (e) {
|
|
606
615
|
console.error("Mermaid scene render failed", e);
|
|
607
616
|
}
|
|
608
617
|
}
|
|
618
|
+
|
|
619
|
+
// Mermaid 11.15.0 does not expose theme variables for C4 boundary/relation
|
|
620
|
+
// paint or its arrow markers. Repair only those known renderer defaults, and
|
|
621
|
+
// leave diagrams with explicit C4 style updates untouched so source colors
|
|
622
|
+
// remain authoritative.
|
|
623
|
+
function repairC4ThemeDefaults(svg, source, themeVariables) {
|
|
624
|
+
if (svg.getAttribute("aria-roledescription") !== "c4") return;
|
|
625
|
+
const commands = source.replace(/^\s*%%.*$/gm, "");
|
|
626
|
+
const hasElementStyles = /\bUpdateElementStyle\s*\(/i.test(commands);
|
|
627
|
+
const hasRelationStyles = /\bUpdateRelStyle\s*\(/i.test(commands);
|
|
628
|
+
const boundaryGroups = new Set(
|
|
629
|
+
[...svg.querySelectorAll('rect[stroke-dasharray="7.0,7.0"], rect[fill="none"]')]
|
|
630
|
+
.map((element) => element.parentElement),
|
|
631
|
+
);
|
|
632
|
+
const relationGroups = new Set(
|
|
633
|
+
[...svg.querySelectorAll("[marker-end], [marker-start]")].map((element) => element.parentElement),
|
|
634
|
+
);
|
|
635
|
+
const boundaryPaint = new Set(["#444", "#444444", "rgb(68, 68, 68)"]);
|
|
636
|
+
const relationPaint = new Set(["#444", "#444444", "rgb(68, 68, 68)"]);
|
|
637
|
+
const setDefaultPaint = (element, property, defaults, value) => {
|
|
638
|
+
const current = element.getAttribute(property)?.trim().toLowerCase();
|
|
639
|
+
if (!defaults.has(current)) return;
|
|
640
|
+
element.setAttribute(property, value);
|
|
641
|
+
if (element.style?.getPropertyValue(property)) {
|
|
642
|
+
element.style.setProperty(property, value);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
for (const element of svg.querySelectorAll("rect, line, path, text")) {
|
|
647
|
+
const parent = element.parentElement;
|
|
648
|
+
if (!hasElementStyles && boundaryGroups.has(parent)) {
|
|
649
|
+
setDefaultPaint(element, "stroke", boundaryPaint, themeVariables.lineColor);
|
|
650
|
+
setDefaultPaint(element, "fill", boundaryPaint, themeVariables.textColor);
|
|
651
|
+
}
|
|
652
|
+
if (!hasRelationStyles && relationGroups.has(parent)) {
|
|
653
|
+
setDefaultPaint(element, "stroke", relationPaint, themeVariables.lineColor);
|
|
654
|
+
setDefaultPaint(element, "fill", relationPaint, themeVariables.textColor);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!hasRelationStyles) {
|
|
658
|
+
for (const marker of svg.querySelectorAll("marker")) {
|
|
659
|
+
for (const element of marker.querySelectorAll("path, polygon, line")) {
|
|
660
|
+
setDefaultPaint(element, "stroke", new Set(["black", "#000", "#000000", "rgb(0, 0, 0)"]), themeVariables.lineColor);
|
|
661
|
+
setDefaultPaint(element, "fill", new Set([undefined, "black", "#000", "#000000", "rgb(0, 0, 0)"]), themeVariables.lineColor);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
609
666
|
})
|
|
610
667
|
.finally(reveal);
|
|
611
668
|
} catch (e) {
|
|
@@ -615,26 +672,28 @@ function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
|
615
672
|
}
|
|
616
673
|
}
|
|
617
674
|
|
|
675
|
+
function withoutMermaidLoadingVeil(callback) {
|
|
676
|
+
const loading = document.body.classList.contains("mermaid-loading");
|
|
677
|
+
if (loading) document.body.classList.remove("mermaid-loading");
|
|
678
|
+
try {
|
|
679
|
+
return callback();
|
|
680
|
+
} finally {
|
|
681
|
+
if (loading) document.body.classList.add("mermaid-loading");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
618
685
|
function renderMermaidScene(svg, deck, blockIndex) {
|
|
619
|
-
const result =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
686
|
+
const result = withoutMermaidLoadingVeil(() =>
|
|
687
|
+
mermaidSvgToScene(svg, {
|
|
688
|
+
path: `mermaid[${blockIndex}]`,
|
|
689
|
+
deck,
|
|
690
|
+
includeSourceElements: true,
|
|
691
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
625
694
|
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
|
-
};
|
|
695
|
+
const capture = (element, options) =>
|
|
696
|
+
withoutMermaidLoadingVeil(() => captureSvgTree(element, options));
|
|
638
697
|
const slots = new Map();
|
|
639
698
|
try {
|
|
640
699
|
for (const [index, node] of scene.nodes.entries()) {
|
|
@@ -657,7 +716,7 @@ function renderMermaidScene(svg, deck, blockIndex) {
|
|
|
657
716
|
node.meta = { ...node.meta, svgOwner: slots.get(source) };
|
|
658
717
|
continue;
|
|
659
718
|
}
|
|
660
|
-
node.meta = { ...node.meta, svg:
|
|
719
|
+
node.meta = { ...node.meta, svg: capture(source) };
|
|
661
720
|
slots.set(source, index);
|
|
662
721
|
}
|
|
663
722
|
for (const [source, index] of slots) {
|
|
@@ -676,14 +735,14 @@ function renderMermaidScene(svg, deck, blockIndex) {
|
|
|
676
735
|
kind: "fallback", sourcePath: "svg", z: 0,
|
|
677
736
|
bounds: { x: 0, y: 0, width: scene.width, height: scene.height },
|
|
678
737
|
capability: { pptx: "fallback", reason }, reason,
|
|
679
|
-
meta: { svg:
|
|
738
|
+
meta: { svg: capture(svg) },
|
|
680
739
|
}] };
|
|
681
740
|
slots.clear();
|
|
682
741
|
slots.set(svg, 0);
|
|
683
742
|
}
|
|
684
743
|
const template = slots.has(svg)
|
|
685
744
|
? { sceneNode: slots.get(svg) }
|
|
686
|
-
:
|
|
745
|
+
: capture(svg, { slots });
|
|
687
746
|
scene.meta = { ...scene.meta, svgRoot: template };
|
|
688
747
|
svg.replaceWith(sceneToSvg(scene, { document, template }));
|
|
689
748
|
}
|
|
@@ -698,7 +757,12 @@ function moveLeadingSlideTitle(header, bodyEl, specialLayout) {
|
|
|
698
757
|
return title;
|
|
699
758
|
}
|
|
700
759
|
|
|
701
|
-
function createSlide(
|
|
760
|
+
function createSlide(
|
|
761
|
+
markdown,
|
|
762
|
+
fallbackTheme,
|
|
763
|
+
themeLocked = deckThemeLocked,
|
|
764
|
+
{ backgroundImage: backgroundOverride } = {},
|
|
765
|
+
) {
|
|
702
766
|
const placeholder = !nonEmpty(markdown);
|
|
703
767
|
const md = placeholder ? PLACEHOLDER : markdown;
|
|
704
768
|
const { meta, body: rawBody } = splitFrontMatter(md);
|
|
@@ -721,6 +785,11 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
721
785
|
// as well as <html> so print mode can render differently themed pages together.
|
|
722
786
|
const theme = normalizeTheme(themeLocked ? fallbackTheme : meta.theme || fallbackTheme);
|
|
723
787
|
const themeMetadata = theme === "custom" ? customThemeMeta : null;
|
|
788
|
+
const backgroundImage = parseSlideBackground(backgroundOverride ?? meta["background-image"]);
|
|
789
|
+
const backgroundUrl = backgroundImage
|
|
790
|
+
? localAssetUrl(backgroundImage.replace(/^\/assets\//, "/background-assets/")
|
|
791
|
+
.split("/").map(encodeURIComponent).join("/"))
|
|
792
|
+
: "";
|
|
724
793
|
|
|
725
794
|
const deck = document.createElement("div");
|
|
726
795
|
deck.className = "deck";
|
|
@@ -732,13 +801,18 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
732
801
|
if (placeholder) deck.classList.add("markdstage-placeholder");
|
|
733
802
|
if (sizeMode !== "auto") setSizeLevel(deck, sizeMode);
|
|
734
803
|
|
|
804
|
+
const background = themeImage(
|
|
805
|
+
backgroundUrl
|
|
806
|
+
? { image: backgroundUrl }
|
|
807
|
+
: resolveThemeBackground(themeMetadata, layout),
|
|
808
|
+
titleSlide ? "slide-background theme-cover-background" : "slide-background",
|
|
809
|
+
{ decorative: true },
|
|
810
|
+
);
|
|
811
|
+
if (background) {
|
|
812
|
+
deck.classList.add("has-slide-background");
|
|
813
|
+
deck.appendChild(background);
|
|
814
|
+
}
|
|
735
815
|
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
816
|
const logo = themeImage(themeMetadata?.cover?.logo, "theme-cover-logo");
|
|
743
817
|
if (logo) deck.appendChild(logo);
|
|
744
818
|
}
|
|
@@ -870,6 +944,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
870
944
|
deck,
|
|
871
945
|
bodyEl,
|
|
872
946
|
theme,
|
|
947
|
+
backgroundImage,
|
|
873
948
|
sizeMode,
|
|
874
949
|
titleSlide,
|
|
875
950
|
sectionSlide,
|
|
@@ -889,6 +964,19 @@ function renderSlide(markdown) {
|
|
|
889
964
|
architectureEditors = [];
|
|
890
965
|
applyCustomThemeCss(customThemeCss);
|
|
891
966
|
const token = ++renderToken;
|
|
967
|
+
if (!surfaceMode && (fixedPreviewMode || presenterViewOpen)) {
|
|
968
|
+
const { meta } = splitFrontMatter(lastMarkdown);
|
|
969
|
+
document.title = meta.title || meta.deck || "Slide";
|
|
970
|
+
document.documentElement.setAttribute("data-theme",
|
|
971
|
+
deckThemeLocked ? deckTheme : normalizeTheme(meta.theme || deckTheme));
|
|
972
|
+
layoutTarget = null;
|
|
973
|
+
surfaceDiagnostic = null;
|
|
974
|
+
if (layoutFrame) cancelAnimationFrame(layoutFrame);
|
|
975
|
+
layoutFrame = 0;
|
|
976
|
+
if (!presenterViewRequested || !presenterViewAvailable || navTotal <= 0) updateSlideViewports();
|
|
977
|
+
updateFixedPreviewWarning();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
892
980
|
const slide = createSlide(markdown, deckTheme);
|
|
893
981
|
document.title = slide.title;
|
|
894
982
|
document.documentElement.setAttribute("data-theme", slide.theme);
|
|
@@ -917,14 +1005,30 @@ function renderSlide(markdown) {
|
|
|
917
1005
|
// that the slide is fully painted, and PDF export and the visual regression
|
|
918
1006
|
// suite rely on it. Revealing while an architecture icon under `assets/` is
|
|
919
1007
|
// still loading would capture a half-drawn slide.
|
|
920
|
-
const images = waitForImages(slide.deck)
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1008
|
+
const images = waitForImages(slide.deck)
|
|
1009
|
+
.then(() => {
|
|
1010
|
+
if (token === renderToken) scheduleLayoutRefresh();
|
|
1011
|
+
})
|
|
1012
|
+
.catch((error) => {
|
|
1013
|
+
if (token !== renderToken) return;
|
|
1014
|
+
console.error(error.message);
|
|
1015
|
+
if (surfaceMode) {
|
|
1016
|
+
window.frameElement?.dispatchEvent(new CustomEvent("slide-error", { detail: error.message }));
|
|
1017
|
+
} else {
|
|
1018
|
+
showExportNotification("error", error.message);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
const mermaid = Promise.resolve(document.fonts?.ready).then(() => {
|
|
1022
|
+
if (token === renderToken) return runMermaid(slide.bodyEl, slide.deck, token, false);
|
|
1023
|
+
}).finally(() => {
|
|
924
1024
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
925
1025
|
});
|
|
926
|
-
Promise.all([mermaid, images]).finally(() => {
|
|
927
|
-
|
|
1026
|
+
Promise.all([mermaid, images]).finally(async () => {
|
|
1027
|
+
await afterLayout();
|
|
1028
|
+
if (token === renderToken) {
|
|
1029
|
+
document.body.classList.remove("mermaid-loading");
|
|
1030
|
+
refreshLayout();
|
|
1031
|
+
}
|
|
928
1032
|
});
|
|
929
1033
|
}
|
|
930
1034
|
|
|
@@ -961,7 +1065,14 @@ function waitForImages(root) {
|
|
|
961
1065
|
}),
|
|
962
1066
|
);
|
|
963
1067
|
}
|
|
964
|
-
return Promise.all(pending)
|
|
1068
|
+
return Promise.all(pending).then(() => {
|
|
1069
|
+
const failedBackground = [...root.querySelectorAll("img.slide-background")].find(
|
|
1070
|
+
(image) => !image.naturalWidth,
|
|
1071
|
+
);
|
|
1072
|
+
if (failedBackground) {
|
|
1073
|
+
throw new Error(`Could not load slide background: ${failedBackground.getAttribute("src")}`);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
965
1076
|
}
|
|
966
1077
|
|
|
967
1078
|
async function reportOutputStatus(token, status, error = "", layout = null) {
|
|
@@ -2033,7 +2144,7 @@ function markMermaidNativeElements(svg, mappedElements, pathPrefix, sourceElemen
|
|
|
2033
2144
|
source = sourceElements.get(ownerPath);
|
|
2034
2145
|
}
|
|
2035
2146
|
if (source && !source.hasAttribute("data-pptx-native")) {
|
|
2036
|
-
const nativeKind = element.type === "shape" ? "shape" : element.type;
|
|
2147
|
+
const nativeKind = element.mermaid?.nativeMask || (element.type === "shape" ? "shape" : element.type);
|
|
2037
2148
|
source.setAttribute("data-pptx-native", nativeKind);
|
|
2038
2149
|
}
|
|
2039
2150
|
if (!sourceElements && element.type === "connector") {
|
|
@@ -2098,10 +2209,17 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2098
2209
|
mermaidElementForSourcePath(svg, sourcePath) ||
|
|
2099
2210
|
mermaidFallbackElementForBounds(svg, deck, fallback) ||
|
|
2100
2211
|
svg;
|
|
2101
|
-
const sourceBounds = source
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2212
|
+
const sourceBounds = fallbackBounds(source, deck, 0, true);
|
|
2213
|
+
const inferredPadding = Math.max(
|
|
2214
|
+
0,
|
|
2215
|
+
(fallback.width - sourceBounds.width) / 2,
|
|
2216
|
+
(fallback.height - sourceBounds.height) / 2,
|
|
2217
|
+
);
|
|
2218
|
+
const geometryPadding = ["path", "line", "polyline"].includes(source.localName)
|
|
2219
|
+
? Math.max(1, inferredPadding)
|
|
2220
|
+
: inferredPadding;
|
|
2221
|
+
const effectPadding = subtreeEffectPaintPadding(source);
|
|
2222
|
+
const padding = Math.max(geometryPadding, effectPadding);
|
|
2105
2223
|
const captured = pptxFallback("mermaid", source, deck, fallback.reason, {
|
|
2106
2224
|
captureElement: source,
|
|
2107
2225
|
includeDescendants: true,
|
|
@@ -2113,10 +2231,14 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2113
2231
|
path: fallback.path,
|
|
2114
2232
|
sourcePath,
|
|
2115
2233
|
reason: fallback.reason,
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2234
|
+
...(effectPadding
|
|
2235
|
+
? {}
|
|
2236
|
+
: {
|
|
2237
|
+
x: fallback.x,
|
|
2238
|
+
y: fallback.y,
|
|
2239
|
+
width: fallback.width,
|
|
2240
|
+
height: fallback.height,
|
|
2241
|
+
}),
|
|
2120
2242
|
zOrder: fallback.zOrder,
|
|
2121
2243
|
...(node?.id ? { id: node.id } : {}),
|
|
2122
2244
|
...(fallback.artwork === false ? { artwork: false } : {}),
|
|
@@ -2125,7 +2247,7 @@ function collectMermaidObjects(element, deck, blockIndex) {
|
|
|
2125
2247
|
return { elements: mapped.elements, fallbacks };
|
|
2126
2248
|
}
|
|
2127
2249
|
|
|
2128
|
-
async function collectPptxSlide(slide, index) {
|
|
2250
|
+
async function collectPptxSlide(slide, index, options = {}) {
|
|
2129
2251
|
const { deck } = slide;
|
|
2130
2252
|
assignPptxPaintOrder(deck);
|
|
2131
2253
|
const elements = [];
|
|
@@ -2232,6 +2354,12 @@ async function collectPptxSlide(slide, index) {
|
|
|
2232
2354
|
for (const [blockIndex, element] of [...deck.querySelectorAll("pre.mermaid")].entries()) {
|
|
2233
2355
|
const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
2234
2356
|
if (covered) continue;
|
|
2357
|
+
if (options.mermaidImageFallback === true) {
|
|
2358
|
+
// The scroll container clips the diagram; SVG definitions have no painted bounds.
|
|
2359
|
+
// The capture pipeline trims transparent space inside this safe envelope.
|
|
2360
|
+
addFallback("mermaid", element, "mermaid-rendered-as-artwork");
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2235
2363
|
try {
|
|
2236
2364
|
const mermaid = collectMermaidObjects(element, deck, blockIndex);
|
|
2237
2365
|
elements.push(...mermaid.elements);
|
|
@@ -2496,7 +2624,7 @@ async function collectPptxSlide(slide, index) {
|
|
|
2496
2624
|
for (const image of deck.querySelectorAll("img")) {
|
|
2497
2625
|
if (
|
|
2498
2626
|
image.closest(".architecture-diagram") ||
|
|
2499
|
-
image.classList.contains("
|
|
2627
|
+
image.classList.contains("slide-background") ||
|
|
2500
2628
|
image.classList.contains("theme-cover-logo") ||
|
|
2501
2629
|
insideFallback(image)
|
|
2502
2630
|
) {
|
|
@@ -2597,20 +2725,20 @@ async function collectPptxSlide(slide, index) {
|
|
|
2597
2725
|
};
|
|
2598
2726
|
}
|
|
2599
2727
|
|
|
2600
|
-
function createPptxLayoutTemplate(theme, layout) {
|
|
2728
|
+
function createPptxLayoutTemplate(theme, layout, id = `${theme}:${layout}`, backgroundImage) {
|
|
2601
2729
|
const markdown = `---
|
|
2602
2730
|
layout: ${layout}
|
|
2603
2731
|
theme: ${theme}
|
|
2604
2732
|
---
|
|
2605
2733
|
`;
|
|
2606
|
-
const slide = createSlide(markdown, theme, true);
|
|
2734
|
+
const slide = createSlide(markdown, theme, true, { backgroundImage });
|
|
2607
2735
|
if (layout === "backcover") {
|
|
2608
2736
|
slide.deck
|
|
2609
2737
|
.querySelectorAll(".theme-backcover-logo, .theme-backcover-copyright")
|
|
2610
2738
|
.forEach((element) => element.remove());
|
|
2611
2739
|
}
|
|
2612
2740
|
slide.deck.classList.add("pptx-layout-template");
|
|
2613
|
-
slide.deck.dataset.pptxLayoutId =
|
|
2741
|
+
slide.deck.dataset.pptxLayoutId = id;
|
|
2614
2742
|
return slide;
|
|
2615
2743
|
}
|
|
2616
2744
|
|
|
@@ -2620,6 +2748,7 @@ async function renderPptxDeck(
|
|
|
2620
2748
|
customCss = "",
|
|
2621
2749
|
themeMetadata = null,
|
|
2622
2750
|
themeLocked = false,
|
|
2751
|
+
options = {},
|
|
2623
2752
|
) {
|
|
2624
2753
|
deckTheme = normalizeTheme(theme);
|
|
2625
2754
|
deckThemeLocked = Boolean(themeLocked);
|
|
@@ -2655,7 +2784,7 @@ async function renderPptxDeck(
|
|
|
2655
2784
|
|
|
2656
2785
|
const pptxSlides = [];
|
|
2657
2786
|
for (const [index, slide] of rendered.entries()) {
|
|
2658
|
-
pptxSlides.push(await collectPptxSlide(slide, index));
|
|
2787
|
+
pptxSlides.push(await collectPptxSlide(slide, index, options));
|
|
2659
2788
|
}
|
|
2660
2789
|
const themes = [...new Set(rendered.map((slide) => slide.theme))];
|
|
2661
2790
|
const layoutTemplates = themes.flatMap((slideTheme) =>
|
|
@@ -2666,6 +2795,26 @@ async function renderPptxDeck(
|
|
|
2666
2795
|
slide: createPptxLayoutTemplate(slideTheme, layout),
|
|
2667
2796
|
})),
|
|
2668
2797
|
);
|
|
2798
|
+
// Per-slide overrides need their own layout artwork, not a background shared
|
|
2799
|
+
// with every slide of the same theme and layout.
|
|
2800
|
+
const backgroundLayouts = new Map();
|
|
2801
|
+
for (const [index, slide] of rendered.entries()) {
|
|
2802
|
+
if (!slide.backgroundImage) continue;
|
|
2803
|
+
const model = pptxSlides[index];
|
|
2804
|
+
const key = JSON.stringify([slide.theme, model.layout, slide.backgroundImage]);
|
|
2805
|
+
let id = backgroundLayouts.get(key);
|
|
2806
|
+
if (!id) {
|
|
2807
|
+
id = `${model.layoutId}:background-${backgroundLayouts.size + 1}`;
|
|
2808
|
+
backgroundLayouts.set(key, id);
|
|
2809
|
+
layoutTemplates.push({
|
|
2810
|
+
id,
|
|
2811
|
+
name: model.layout,
|
|
2812
|
+
theme: slide.theme,
|
|
2813
|
+
slide: createPptxLayoutTemplate(slide.theme, model.layout, id, slide.backgroundImage),
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
model.layoutId = id;
|
|
2817
|
+
}
|
|
2669
2818
|
stage.append(...layoutTemplates.map((layout) => layout.slide.deck));
|
|
2670
2819
|
await waitForImages(stage);
|
|
2671
2820
|
await afterLayout();
|
|
@@ -2683,7 +2832,7 @@ async function renderPptxDeck(
|
|
|
2683
2832
|
masters: themes.map((slideTheme) => ({
|
|
2684
2833
|
id: slideTheme,
|
|
2685
2834
|
theme: slideTheme,
|
|
2686
|
-
layoutIds:
|
|
2835
|
+
layoutIds: pptxLayouts.filter((layout) => layout.theme === slideTheme).map((layout) => layout.id),
|
|
2687
2836
|
})),
|
|
2688
2837
|
layouts: pptxLayouts,
|
|
2689
2838
|
slides: pptxSlides,
|
|
@@ -2721,6 +2870,7 @@ async function initPptx(params) {
|
|
|
2721
2870
|
data.customThemeCss,
|
|
2722
2871
|
data.customThemeMeta,
|
|
2723
2872
|
data.themeLocked,
|
|
2873
|
+
{ mermaidImageFallback: data.mermaidImageFallback === true },
|
|
2724
2874
|
);
|
|
2725
2875
|
await reportOutputStatus(token, "ready", "", output.layout);
|
|
2726
2876
|
} catch (error) {
|
|
@@ -3021,7 +3171,7 @@ async function fetchDeck() {
|
|
|
3021
3171
|
function setArchitectureEditMode(enabled) {
|
|
3022
3172
|
const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
|
|
3023
3173
|
if (next === architectureEditMode) return false;
|
|
3024
|
-
if (next && fixedPreviewMode) setFixedPreviewMode(false);
|
|
3174
|
+
if (next && fixedPreviewMode) setFixedPreviewMode(false, { rerender: false });
|
|
3025
3175
|
architectureEditMode = next;
|
|
3026
3176
|
document.body.classList.toggle("architecture-edit-mode", next);
|
|
3027
3177
|
updateArchitectureEditButton(next);
|
|
@@ -3127,7 +3277,8 @@ async function requestArchitectureEditMode(enabled) {
|
|
|
3127
3277
|
}
|
|
3128
3278
|
|
|
3129
3279
|
function architectureDiagramOptions() {
|
|
3130
|
-
|
|
3280
|
+
const slideDocument = document.getElementById("outputFrame")?.contentDocument || document;
|
|
3281
|
+
return [...slideDocument.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
|
|
3131
3282
|
(wrapper, index) => ({
|
|
3132
3283
|
block: Number(wrapper.dataset.architectureBlock),
|
|
3133
3284
|
title: wrapper.dataset.architectureTitle || `Diagram ${index + 1}`,
|
|
@@ -3453,6 +3604,10 @@ async function fetchState() {
|
|
|
3453
3604
|
// re-fetches /state for an instant update (without waiting for the SSE nudge).
|
|
3454
3605
|
async function navigate(payload) {
|
|
3455
3606
|
if (!navigationEnabled) return;
|
|
3607
|
+
if (surfaceMode) {
|
|
3608
|
+
window.frameElement?.dispatchEvent(new CustomEvent("slide-navigate", { detail: payload }));
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3456
3611
|
try {
|
|
3457
3612
|
const res = await fetch("./navigate", {
|
|
3458
3613
|
method: "POST",
|
|
@@ -3681,9 +3836,45 @@ function showExportNotification(state, message, path = "") {
|
|
|
3681
3836
|
resumeExportNotification();
|
|
3682
3837
|
}
|
|
3683
3838
|
|
|
3684
|
-
|
|
3839
|
+
let pptxOptionsPending = false;
|
|
3840
|
+
|
|
3841
|
+
async function requestPptxExport() {
|
|
3842
|
+
const dialog = document.getElementById("pptxExportDialog");
|
|
3843
|
+
if (exportPending || pptxOptionsPending || dialog.open || !pptxExportAvailable) return;
|
|
3844
|
+
pptxOptionsPending = true;
|
|
3845
|
+
let hasMermaid;
|
|
3846
|
+
try {
|
|
3847
|
+
const response = await fetch("./deck", { cache: "no-store" });
|
|
3848
|
+
if (!response.ok) throw new Error(`Could not load the export deck (${response.status}).`);
|
|
3849
|
+
const data = await response.json();
|
|
3850
|
+
if (!Array.isArray(data?.slides) || !data.slides.every((slide) => typeof slide === "string")) {
|
|
3851
|
+
throw new Error("The export deck is invalid.");
|
|
3852
|
+
}
|
|
3853
|
+
const slides = [...data.slides];
|
|
3854
|
+
if (navMode === "adhoc") slides[Math.max(0, Math.min(navIndex, slides.length - 1))] = lastMarkdown;
|
|
3855
|
+
hasMermaid = slides.some((markdown) => {
|
|
3856
|
+
const { body } = splitFrontMatter(markdown);
|
|
3857
|
+
const content = document.createElement("div");
|
|
3858
|
+
content.innerHTML = window.DOMPurify.sanitize(window.marked.parse(stripSpeakerNotes(body)));
|
|
3859
|
+
return codeBlocksForLanguage(content, "mermaid").length > 0 || Boolean(content.querySelector(".mermaid"));
|
|
3860
|
+
});
|
|
3861
|
+
} catch (error) {
|
|
3862
|
+
showExportNotification("error", `Could not save PowerPoint. ${error.message}`);
|
|
3863
|
+
return;
|
|
3864
|
+
} finally {
|
|
3865
|
+
pptxOptionsPending = false;
|
|
3866
|
+
}
|
|
3867
|
+
if (hasMermaid) {
|
|
3868
|
+
document.getElementById("pptxExportForm").reset();
|
|
3869
|
+
dialog.showModal();
|
|
3870
|
+
} else {
|
|
3871
|
+
await exportFromCanvas("pptx");
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
async function exportFromCanvas(format, { mermaidImageFallback = false } = {}) {
|
|
3685
3876
|
const isPdf = format === "pdf";
|
|
3686
|
-
if (exportPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3877
|
+
if (exportPending || pptxOptionsPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3687
3878
|
exportPending = true;
|
|
3688
3879
|
const label = isPdf ? "PDF" : "PowerPoint";
|
|
3689
3880
|
const pdfButton = document.getElementById("navExport");
|
|
@@ -3700,7 +3891,11 @@ async function exportFromCanvas(format) {
|
|
|
3700
3891
|
try {
|
|
3701
3892
|
const response = await fetch(isPdf ? "./export" : "./export-pptx", {
|
|
3702
3893
|
method: "POST",
|
|
3703
|
-
headers: {
|
|
3894
|
+
headers: {
|
|
3895
|
+
Accept: "application/json",
|
|
3896
|
+
...(isPdf ? {} : { "Content-Type": "application/json" }),
|
|
3897
|
+
},
|
|
3898
|
+
...(isPdf ? {} : { body: JSON.stringify({ mermaidImageFallback }) }),
|
|
3704
3899
|
cache: "no-store",
|
|
3705
3900
|
});
|
|
3706
3901
|
const data = await response.json();
|
|
@@ -3732,26 +3927,29 @@ async function exportFromCanvas(format) {
|
|
|
3732
3927
|
}
|
|
3733
3928
|
}
|
|
3734
3929
|
|
|
3735
|
-
function setFixedPreviewMode(enabled) {
|
|
3736
|
-
fixedPreviewMode = Boolean(enabled);
|
|
3930
|
+
function setFixedPreviewMode(enabled, { rerender = true } = {}) {
|
|
3931
|
+
fixedPreviewMode = presenterMode || Boolean(enabled);
|
|
3737
3932
|
document.body.classList.toggle("fixed-preview-mode", fixedPreviewMode);
|
|
3738
|
-
document.body.classList.toggle("
|
|
3933
|
+
document.body.classList.toggle("responsive-preview-mode", !fixedPreviewMode);
|
|
3739
3934
|
const button = document.getElementById("navFixedPreview");
|
|
3740
3935
|
if (button) {
|
|
3741
3936
|
button.setAttribute("aria-pressed", fixedPreviewMode ? "true" : "false");
|
|
3742
3937
|
button.dataset.state = fixedPreviewMode ? "active" : "";
|
|
3743
3938
|
button.title = fixedPreviewMode
|
|
3744
|
-
? "
|
|
3939
|
+
? "Switch to responsive editing layout (not output-equivalent)"
|
|
3745
3940
|
: "Preview PDF layout at 16:9";
|
|
3941
|
+
button.querySelector(".nav-more-label").textContent =
|
|
3942
|
+
fixedPreviewMode ? "Output preview" : "Responsive editing";
|
|
3746
3943
|
}
|
|
3747
3944
|
syncMoreControls();
|
|
3748
|
-
if (fixedPreviewMode) {
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
document.
|
|
3945
|
+
if (!fixedPreviewMode) {
|
|
3946
|
+
outputViewport?.dispose();
|
|
3947
|
+
outputViewport = null;
|
|
3948
|
+
document.getElementById("stage").classList.remove("slide-viewport");
|
|
3752
3949
|
document.body.classList.remove("fixed-preview-overflow");
|
|
3753
3950
|
}
|
|
3754
|
-
|
|
3951
|
+
surfaceDiagnostic = null;
|
|
3952
|
+
if (rerender && currentVersion >= 0) renderSlide(lastMarkdown);
|
|
3755
3953
|
updateFixedPreviewWarning();
|
|
3756
3954
|
}
|
|
3757
3955
|
|
|
@@ -3759,6 +3957,103 @@ function toggleFixedPreviewMode() {
|
|
|
3759
3957
|
setFixedPreviewMode(!fixedPreviewMode);
|
|
3760
3958
|
}
|
|
3761
3959
|
|
|
3960
|
+
function slideViewportState(markdown, index, interactive) {
|
|
3961
|
+
return {
|
|
3962
|
+
markdown, index, total: navTotal, theme: deckTheme, themeLocked: deckThemeLocked,
|
|
3963
|
+
customThemeCss, customThemeMeta, navigationEnabled: interactive,
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function mountSlideViewport(host, id, title, interactive) {
|
|
3968
|
+
return createSlideViewport(host, {
|
|
3969
|
+
id, title,
|
|
3970
|
+
onNavigate: (payload) => { if (interactive) navigate(payload); },
|
|
3971
|
+
onPointer: () => { if (interactive) setMoreControlsOpen(false); },
|
|
3972
|
+
onError: (message) => showExportNotification("error", message),
|
|
3973
|
+
onKey: (detail) => {
|
|
3974
|
+
if (!interactive) return;
|
|
3975
|
+
const event = new KeyboardEvent("keydown", { ...detail, bubbles: true, cancelable: true });
|
|
3976
|
+
document.dispatchEvent(event);
|
|
3977
|
+
detail.handled = event.defaultPrevented;
|
|
3978
|
+
},
|
|
3979
|
+
onLayout: (diagnostic) => {
|
|
3980
|
+
if (id === "presenterNext") return;
|
|
3981
|
+
surfaceDiagnostic = diagnostic;
|
|
3982
|
+
document.body.classList.remove("mermaid-loading");
|
|
3983
|
+
updateFixedPreviewWarning();
|
|
3984
|
+
},
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function updateSlideViewports() {
|
|
3989
|
+
const stage = document.getElementById("stage");
|
|
3990
|
+
document.body.classList.add("mermaid-loading");
|
|
3991
|
+
if (presenterViewOpen) {
|
|
3992
|
+
stage.replaceChildren();
|
|
3993
|
+
currentViewport ??= mountSlideViewport(
|
|
3994
|
+
document.getElementById("presenterCurrentViewport"), "presenterCurrent", "Current slide", true,
|
|
3995
|
+
);
|
|
3996
|
+
currentViewport.setState(slideViewportState(lastMarkdown, navIndex, true));
|
|
3997
|
+
const hasNext = navMode !== "deck" || navIndex < navTotal - 1;
|
|
3998
|
+
if (hasNext) {
|
|
3999
|
+
const index = Math.min(navIndex + 1, navTotal - 1);
|
|
4000
|
+
nextViewport ??= mountSlideViewport(
|
|
4001
|
+
document.getElementById("presenterNextViewport"), "presenterNext", "Next slide", false,
|
|
4002
|
+
);
|
|
4003
|
+
nextViewport.setState(slideViewportState(deckSlides[index] || "", index, false));
|
|
4004
|
+
}
|
|
4005
|
+
} else {
|
|
4006
|
+
if (!outputViewport) {
|
|
4007
|
+
stage.replaceChildren();
|
|
4008
|
+
stage.classList.add("slide-viewport");
|
|
4009
|
+
outputViewport = mountSlideViewport(stage, "outputFrame", "Slide", navigationEnabled);
|
|
4010
|
+
}
|
|
4011
|
+
outputViewport.setState(slideViewportState(lastMarkdown, navIndex, navigationEnabled));
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
function initSlideSurface() {
|
|
4016
|
+
surfaceMode = true;
|
|
4017
|
+
presenterMode = true;
|
|
4018
|
+
document.body.classList.add("surface-mode", "fixed-output-mode", "presenter-mode", "preview-mode");
|
|
4019
|
+
let previousState = "";
|
|
4020
|
+
window.__markdstageSurface = {
|
|
4021
|
+
render(state) {
|
|
4022
|
+
const key = JSON.stringify(state);
|
|
4023
|
+
if (key === previousState) {
|
|
4024
|
+
refreshLayout();
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
previousState = key;
|
|
4028
|
+
deckTheme = state.theme;
|
|
4029
|
+
deckThemeLocked = state.themeLocked;
|
|
4030
|
+
applyCustomThemeCss(state.customThemeCss);
|
|
4031
|
+
customThemeMeta = state.customThemeMeta;
|
|
4032
|
+
navIndex = state.index;
|
|
4033
|
+
navTotal = state.total;
|
|
4034
|
+
navigationEnabled = state.navigationEnabled;
|
|
4035
|
+
renderSlide(state.markdown);
|
|
4036
|
+
},
|
|
4037
|
+
};
|
|
4038
|
+
wirePointerNavigation();
|
|
4039
|
+
document.addEventListener("pointerdown", () => {
|
|
4040
|
+
window.frameElement?.dispatchEvent(new CustomEvent("slide-pointer"));
|
|
4041
|
+
});
|
|
4042
|
+
document.addEventListener("keydown", (event) => {
|
|
4043
|
+
if (!navigationEnabled || event.defaultPrevented) return;
|
|
4044
|
+
const target = event.target;
|
|
4045
|
+
if (target?.closest("input, textarea, select, [contenteditable], button, a")) return;
|
|
4046
|
+
const detail = {
|
|
4047
|
+
key: event.key, code: event.code, repeat: event.repeat,
|
|
4048
|
+
ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, shiftKey: event.shiftKey,
|
|
4049
|
+
};
|
|
4050
|
+
window.frameElement?.dispatchEvent(new CustomEvent("slide-key", { detail }));
|
|
4051
|
+
if (detail.handled) event.preventDefault();
|
|
4052
|
+
});
|
|
4053
|
+
window.addEventListener("resize", scheduleLayoutRefresh);
|
|
4054
|
+
document.fonts?.addEventListener("loadingdone", scheduleLayoutRefresh);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
3762
4057
|
function updateNav() {
|
|
3763
4058
|
const nav = document.getElementById("nav");
|
|
3764
4059
|
if (!nav) return;
|
|
@@ -3788,14 +4083,9 @@ function openPresenterView() {
|
|
|
3788
4083
|
document.body.classList.add("presenter-view-mode");
|
|
3789
4084
|
const view = document.getElementById("presenterView");
|
|
3790
4085
|
if (view) view.hidden = false;
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
current.setAttribute("src", "./?preview=1&offset=0&navigate=1");
|
|
3795
|
-
}
|
|
3796
|
-
if (next && !next.getAttribute("src")) {
|
|
3797
|
-
next.setAttribute("src", "./?preview=1&offset=1");
|
|
3798
|
-
}
|
|
4086
|
+
outputViewport?.dispose();
|
|
4087
|
+
outputViewport = null;
|
|
4088
|
+
renderSlide(lastMarkdown);
|
|
3799
4089
|
updatePresenterView();
|
|
3800
4090
|
}
|
|
3801
4091
|
|
|
@@ -3804,6 +4094,11 @@ function closePresenterView() {
|
|
|
3804
4094
|
document.body.classList.remove("presenter-view-mode");
|
|
3805
4095
|
const view = document.getElementById("presenterView");
|
|
3806
4096
|
if (view) view.hidden = true;
|
|
4097
|
+
currentViewport?.dispose();
|
|
4098
|
+
nextViewport?.dispose();
|
|
4099
|
+
currentViewport = null;
|
|
4100
|
+
nextViewport = null;
|
|
4101
|
+
renderSlide(lastMarkdown);
|
|
3807
4102
|
document.getElementById("navMore")?.focus();
|
|
3808
4103
|
}
|
|
3809
4104
|
|
|
@@ -3816,7 +4111,7 @@ function updatePresenterView() {
|
|
|
3816
4111
|
if (prev) prev.disabled = navMode === "deck" && navIndex <= 0;
|
|
3817
4112
|
if (next) next.disabled = navMode === "deck" && navIndex >= navTotal - 1;
|
|
3818
4113
|
const hasNext = navMode !== "deck" || navIndex < navTotal - 1;
|
|
3819
|
-
const nextFrame = document.getElementById("
|
|
4114
|
+
const nextFrame = document.getElementById("presenterNextViewport");
|
|
3820
4115
|
const nextEmpty = document.getElementById("presenterNextEmpty");
|
|
3821
4116
|
if (nextFrame) nextFrame.hidden = !hasNext;
|
|
3822
4117
|
if (nextEmpty) nextEmpty.hidden = hasNext;
|
|
@@ -4175,7 +4470,7 @@ function wireControls() {
|
|
|
4175
4470
|
bind("navPresenterView", openPresenterView, { closeMore: true });
|
|
4176
4471
|
bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
|
|
4177
4472
|
bind("navExport", () => exportFromCanvas("pdf"), { closeMore: true });
|
|
4178
|
-
bind("navExportPptx",
|
|
4473
|
+
bind("navExportPptx", requestPptxExport, { closeMore: true });
|
|
4179
4474
|
bind("navImport", toggleImportPicker, { closeMore: true });
|
|
4180
4475
|
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
4181
4476
|
bind("overviewClose", closeOverview);
|
|
@@ -4187,6 +4482,29 @@ function wireControls() {
|
|
|
4187
4482
|
bind("presenterToggleButton", togglePresenterWindow);
|
|
4188
4483
|
bind("presenterReturnButton", closePresenterView);
|
|
4189
4484
|
|
|
4485
|
+
const pptxDialog = document.getElementById("pptxExportDialog");
|
|
4486
|
+
pptxDialog.addEventListener("keydown", (event) => {
|
|
4487
|
+
if (event.key !== "Tab") return;
|
|
4488
|
+
const controls = [...pptxDialog.querySelectorAll("input:checked, button:not(:disabled)")];
|
|
4489
|
+
const first = controls[0];
|
|
4490
|
+
const last = controls.at(-1);
|
|
4491
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
4492
|
+
event.preventDefault();
|
|
4493
|
+
last.focus();
|
|
4494
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
4495
|
+
event.preventDefault();
|
|
4496
|
+
first.focus();
|
|
4497
|
+
}
|
|
4498
|
+
});
|
|
4499
|
+
document.getElementById("pptxExportCancel").addEventListener("click", () => pptxDialog.close());
|
|
4500
|
+
pptxDialog.addEventListener("close", () => document.getElementById("navMore").focus());
|
|
4501
|
+
document.getElementById("pptxExportForm").addEventListener("submit", (event) => {
|
|
4502
|
+
event.preventDefault();
|
|
4503
|
+
const mermaidImageFallback = new FormData(event.currentTarget).get("mermaidOutput") === "images";
|
|
4504
|
+
pptxDialog.close();
|
|
4505
|
+
exportFromCanvas("pptx", { mermaidImageFallback });
|
|
4506
|
+
});
|
|
4507
|
+
|
|
4190
4508
|
const exportNotification = document.getElementById("exportNotification");
|
|
4191
4509
|
document.getElementById("exportNotificationClose").addEventListener("click", dismissExportNotification);
|
|
4192
4510
|
exportNotification.addEventListener("pointerenter", pauseExportNotification);
|
|
@@ -4242,6 +4560,7 @@ function wireControls() {
|
|
|
4242
4560
|
|
|
4243
4561
|
document.addEventListener("keydown", (e) => {
|
|
4244
4562
|
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
4563
|
+
if (pptxDialog.open) return;
|
|
4245
4564
|
const t = e.target;
|
|
4246
4565
|
// Keep Esc active while an input has focus; otherwise filtering in the import
|
|
4247
4566
|
// dialog could leave the user unable to close it.
|
|
@@ -4320,6 +4639,10 @@ function init() {
|
|
|
4320
4639
|
} catch (_) {}
|
|
4321
4640
|
|
|
4322
4641
|
const params = new URLSearchParams(window.location.search);
|
|
4642
|
+
if (params.get("surface") === "1") {
|
|
4643
|
+
initSlideSurface();
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4323
4646
|
presenterViewRequested = params.get("presenter") === "1";
|
|
4324
4647
|
if (params.get("pptx") === "1") {
|
|
4325
4648
|
initPptx(params).catch(reportPptxBootstrapFailure);
|
|
@@ -4354,15 +4677,12 @@ function init() {
|
|
|
4354
4677
|
requestArchitectureEditMode(true);
|
|
4355
4678
|
}
|
|
4356
4679
|
|
|
4357
|
-
//
|
|
4358
|
-
//
|
|
4359
|
-
|
|
4360
|
-
if (
|
|
4361
|
-
!presenterMode &&
|
|
4362
|
-
!presenterViewRequested &&
|
|
4363
|
-
params.get("responsive") !== "1"
|
|
4364
|
-
) {
|
|
4680
|
+
// Only the explicitly requested editing layout is responsive. Audience and
|
|
4681
|
+
// Desktop previews use the same isolated surface as Canvas and CLI previews.
|
|
4682
|
+
if (presenterMode || params.get("responsive") !== "1") {
|
|
4365
4683
|
setFixedPreviewMode(true);
|
|
4684
|
+
} else {
|
|
4685
|
+
setFixedPreviewMode(false);
|
|
4366
4686
|
}
|
|
4367
4687
|
|
|
4368
4688
|
updateArchitectureEditButton();
|
|
@@ -4372,7 +4692,6 @@ function init() {
|
|
|
4372
4692
|
wirePreviewKeyboardNavigation();
|
|
4373
4693
|
}
|
|
4374
4694
|
window.addEventListener("resize", () => {
|
|
4375
|
-
updateFixedPreviewScale();
|
|
4376
4695
|
scheduleLayoutRefresh();
|
|
4377
4696
|
});
|
|
4378
4697
|
if (document.fonts?.ready) {
|