@markdstage/markdstage 3.2.0 → 3.4.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 +23 -12
- package/package.json +1 -1
- package/shared/README.md +18 -7
- package/shared/renderer/architecture-scene.mjs +312 -0
- package/shared/renderer/architecture.mjs +41 -18
- package/shared/renderer/index.html +9 -1
- package/shared/renderer/mermaid-scene.mjs +1248 -0
- package/shared/renderer/renderer.js +444 -234
- package/shared/renderer/scene-graph.mjs +739 -0
- package/shared/renderer/scene-pptx.mjs +265 -0
- package/shared/renderer/scene-svg.mjs +321 -0
- package/shared/renderer/slides.css +23 -0
- package/shared/renderer/theme.mjs +54 -0
- package/shared/runtime/architecture-editor-server.mjs +3 -0
- package/shared/runtime/deck-session.mjs +37 -6
- package/shared/runtime/pptx-package.mjs +4 -3
- package/shared/runtime/presentation-server.mjs +368 -95
- package/src/cli.mjs +92 -19
- package/src/commands/present.mjs +133 -143
- package/src/deck.mjs +4 -0
- package/src/skills.mjs +12 -8
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { powerPointDashStyle, renderArchitectureBlock } from "./architecture.mjs";
|
|
2
|
+
import { architectureSnapshotToScene } from "./architecture-scene.mjs";
|
|
3
|
+
import { mermaidSvgToScene } from "./mermaid-scene.mjs";
|
|
4
|
+
import { captureSvgTree, sceneToSvg } from "./scene-svg.mjs";
|
|
5
|
+
import { sceneToPptxElements } from "./scene-pptx.mjs";
|
|
2
6
|
import { attachArchitectureEditor } from "./architecture-editor.mjs";
|
|
3
7
|
import {
|
|
4
8
|
DEFAULT_THEME,
|
|
9
|
+
mermaidThemeVariables,
|
|
5
10
|
normalizeTheme,
|
|
6
11
|
parseFrontMatter,
|
|
7
12
|
} from "./theme.mjs";
|
|
@@ -74,11 +79,6 @@ function localAssetUrl(path, documentRef = document) {
|
|
|
74
79
|
// The deck theme is chosen by the agent (load_deck `theme`) and delivered via
|
|
75
80
|
// /state; slide front matter may override it unless the deck theme was explicit.
|
|
76
81
|
// Anything unrecognized falls back to the default so a slide is never unstyled.
|
|
77
|
-
const MERMAID_THEME = {
|
|
78
|
-
dark: "dark",
|
|
79
|
-
light: "default",
|
|
80
|
-
microsoft: "neutral",
|
|
81
|
-
};
|
|
82
82
|
const SIZE_MODES = new Set(["auto", "normal", "large", "xlarge"]);
|
|
83
83
|
const DEFAULT_SIZE_MODE = "auto";
|
|
84
84
|
let deckTheme = DEFAULT_THEME;
|
|
@@ -88,7 +88,7 @@ let customThemeMeta = null;
|
|
|
88
88
|
// Bumped on every render so a late mermaid finish from a previous slide can't
|
|
89
89
|
// reveal a newer, still-rendering one.
|
|
90
90
|
let renderToken = 0;
|
|
91
|
-
let
|
|
91
|
+
let lastMermaidThemeVariables = null;
|
|
92
92
|
let pptxFallbackSequence = 0;
|
|
93
93
|
const pptxFallbackCaptureElements = new Map();
|
|
94
94
|
// Editing mode is available only in normal view, not presenter or print mode.
|
|
@@ -426,13 +426,14 @@ function collectDeckLayout(rendered) {
|
|
|
426
426
|
function updateFixedPreviewWarning() {
|
|
427
427
|
const warning = document.getElementById("layoutWarning");
|
|
428
428
|
const button = document.getElementById("navFixedPreview");
|
|
429
|
-
|
|
429
|
+
const empty = document.body.classList.contains("markdstage-empty");
|
|
430
|
+
if (!fixedPreviewMode || !layoutTarget || empty) {
|
|
430
431
|
document.body.classList.remove("fixed-preview-overflow");
|
|
431
432
|
if (warning) {
|
|
432
433
|
warning.hidden = true;
|
|
433
434
|
warning.textContent = "";
|
|
434
435
|
}
|
|
435
|
-
if (button) button.dataset.state = fixedPreviewMode ? "active" : "";
|
|
436
|
+
if (button) button.dataset.state = fixedPreviewMode && !empty ? "active" : "";
|
|
436
437
|
syncMoreControls();
|
|
437
438
|
return;
|
|
438
439
|
}
|
|
@@ -564,9 +565,12 @@ function applySyntaxHighlighting(root) {
|
|
|
564
565
|
// --- mermaid ---------------------------------------------------------------
|
|
565
566
|
// Render every <pre class="mermaid"> in `scope` to SVG. Resilient: a slide with
|
|
566
567
|
// no diagrams, a missing library, or an invalid diagram must never leave the
|
|
567
|
-
// slide blank, so the body is always revealed in the end.
|
|
568
|
-
//
|
|
569
|
-
|
|
568
|
+
// slide blank, so the body is always revealed in the end. Mermaid's palette is
|
|
569
|
+
// derived from the rendered deck's theme custom properties (see
|
|
570
|
+
// theme.mjs#mermaidThemeVariables) so diagrams match the slide instead of
|
|
571
|
+
// picking the closest built-in Mermaid theme, and it is re-initialized only
|
|
572
|
+
// when the resolved colors actually change.
|
|
573
|
+
function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
570
574
|
// Only the latest render may lift the loading veil; a stale finish is ignored.
|
|
571
575
|
const reveal = () => {
|
|
572
576
|
if (revealWhenDone && token === renderToken) {
|
|
@@ -579,13 +583,30 @@ function runMermaid(scope, theme, token, revealWhenDone = true) {
|
|
|
579
583
|
return Promise.resolve();
|
|
580
584
|
}
|
|
581
585
|
try {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
+
const themeVariables = mermaidThemeVariables(getComputedStyle(deckEl));
|
|
587
|
+
const serializedThemeVariables = JSON.stringify(themeVariables);
|
|
588
|
+
if (serializedThemeVariables !== lastMermaidThemeVariables) {
|
|
589
|
+
window.mermaid.initialize({
|
|
590
|
+
startOnLoad: false,
|
|
591
|
+
theme: "base",
|
|
592
|
+
themeVariables,
|
|
593
|
+
securityLevel: "strict",
|
|
594
|
+
});
|
|
595
|
+
lastMermaidThemeVariables = serializedThemeVariables;
|
|
586
596
|
}
|
|
587
597
|
return Promise.resolve(window.mermaid.run({ nodes }))
|
|
588
598
|
.catch((e) => console.error("Mermaid render failed", e))
|
|
599
|
+
.then(() => {
|
|
600
|
+
for (const [index, host] of [...nodes].entries()) {
|
|
601
|
+
const source = host.querySelector("svg");
|
|
602
|
+
if (!source || source.hasAttribute("data-scene-backend")) continue;
|
|
603
|
+
try {
|
|
604
|
+
renderMermaidScene(source, deckEl, index);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
console.error("Mermaid scene render failed", e);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
})
|
|
589
610
|
.finally(reveal);
|
|
590
611
|
} catch (e) {
|
|
591
612
|
console.error("Mermaid init failed", e);
|
|
@@ -594,6 +615,79 @@ function runMermaid(scope, theme, token, revealWhenDone = true) {
|
|
|
594
615
|
}
|
|
595
616
|
}
|
|
596
617
|
|
|
618
|
+
function renderMermaidScene(svg, deck, blockIndex) {
|
|
619
|
+
const result = mermaidSvgToScene(svg, {
|
|
620
|
+
path: `mermaid[${blockIndex}]`,
|
|
621
|
+
deck,
|
|
622
|
+
includeSourceElements: true,
|
|
623
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
624
|
+
});
|
|
625
|
+
let { scene } = result;
|
|
626
|
+
const computedStyle = (element) => {
|
|
627
|
+
const style = getComputedStyle(element);
|
|
628
|
+
return {
|
|
629
|
+
getPropertyValue(property) {
|
|
630
|
+
// The loading veil is inherited by every SVG descendant, not diagram style.
|
|
631
|
+
if (property === "visibility" && document.body.classList.contains("mermaid-loading")) {
|
|
632
|
+
return element.style?.visibility || element.getAttribute("visibility") || "";
|
|
633
|
+
}
|
|
634
|
+
return style.getPropertyValue(property);
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
const slots = new Map();
|
|
639
|
+
try {
|
|
640
|
+
for (const [index, node] of scene.nodes.entries()) {
|
|
641
|
+
const exactSource = result.sourceElements?.get(node.sourcePath) ||
|
|
642
|
+
mermaidElementForSourcePath(svg, node.sourcePath);
|
|
643
|
+
const owner = scene.nodes.find((candidate) => {
|
|
644
|
+
if (candidate === node || !node.sourcePath.startsWith(`${candidate.sourcePath}.`)) return false;
|
|
645
|
+
const candidateSource = result.sourceElements?.get(candidate.sourcePath) ||
|
|
646
|
+
mermaidElementForSourcePath(svg, candidate.sourcePath);
|
|
647
|
+
return candidateSource && (!exactSource || candidateSource.contains(exactSource));
|
|
648
|
+
});
|
|
649
|
+
if (owner) {
|
|
650
|
+
node.meta = { ...node.meta, svgOwner: owner.sourcePath };
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const source = exactSource ||
|
|
654
|
+
mermaidFallbackElementForBounds(svg, deck, node.bounds);
|
|
655
|
+
if (!source) throw new Error(`Mermaid SVG source unavailable: ${node.sourcePath}`);
|
|
656
|
+
if (slots.has(source)) {
|
|
657
|
+
node.meta = { ...node.meta, svgOwner: slots.get(source) };
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
node.meta = { ...node.meta, svg: captureSvgTree(source, { computedStyle }) };
|
|
661
|
+
slots.set(source, index);
|
|
662
|
+
}
|
|
663
|
+
for (const [source, index] of slots) {
|
|
664
|
+
const ancestor = [...slots.keys()].find((candidate) => candidate !== source && candidate.contains(source));
|
|
665
|
+
if (ancestor) {
|
|
666
|
+
scene.nodes[index].meta.svgOwner = slots.get(ancestor);
|
|
667
|
+
slots.delete(source);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
// A new Mermaid structure must stay visible even if its source mapping is
|
|
672
|
+
// not yet understood. Preserve the full safe artwork and report the reason.
|
|
673
|
+
const reason = `mermaid-svg-source-fallback: ${error.message}`;
|
|
674
|
+
console.warn(reason);
|
|
675
|
+
scene = { ...scene, nodes: [{
|
|
676
|
+
kind: "fallback", sourcePath: "svg", z: 0,
|
|
677
|
+
bounds: { x: 0, y: 0, width: scene.width, height: scene.height },
|
|
678
|
+
capability: { pptx: "fallback", reason }, reason,
|
|
679
|
+
meta: { svg: captureSvgTree(svg, { computedStyle }) },
|
|
680
|
+
}] };
|
|
681
|
+
slots.clear();
|
|
682
|
+
slots.set(svg, 0);
|
|
683
|
+
}
|
|
684
|
+
const template = slots.has(svg)
|
|
685
|
+
? { sceneNode: slots.get(svg) }
|
|
686
|
+
: captureSvgTree(svg, { slots, computedStyle });
|
|
687
|
+
scene.meta = { ...scene.meta, svgRoot: template };
|
|
688
|
+
svg.replaceWith(sceneToSvg(scene, { document, template }));
|
|
689
|
+
}
|
|
690
|
+
|
|
597
691
|
// --- slide rendering -------------------------------------------------------
|
|
598
692
|
function moveLeadingSlideTitle(header, bodyEl, specialLayout) {
|
|
599
693
|
if (specialLayout) return null;
|
|
@@ -826,7 +920,7 @@ function renderSlide(markdown) {
|
|
|
826
920
|
const images = waitForImages(slide.deck).then(() => {
|
|
827
921
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
828
922
|
});
|
|
829
|
-
const mermaid = runMermaid(slide.bodyEl, slide.
|
|
923
|
+
const mermaid = runMermaid(slide.bodyEl, slide.deck, token, false).finally(() => {
|
|
830
924
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
831
925
|
});
|
|
832
926
|
Promise.all([mermaid, images]).finally(() => {
|
|
@@ -1666,51 +1760,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1666
1760
|
width: roundedMetric(object.width * scale),
|
|
1667
1761
|
height: roundedMetric(object.height * scale),
|
|
1668
1762
|
});
|
|
1669
|
-
const
|
|
1670
|
-
const mapped = { ...object };
|
|
1671
|
-
const mapParagraphs = (paragraphs) =>
|
|
1672
|
-
paragraphs.map((paragraph) => ({
|
|
1673
|
-
...paragraph,
|
|
1674
|
-
runs: paragraph.runs.map((run) => ({
|
|
1675
|
-
...run,
|
|
1676
|
-
color: resolveModelColor(run.color, deck),
|
|
1677
|
-
fontFace: getComputedStyle(svg).fontFamily
|
|
1678
|
-
.split(",")[0]
|
|
1679
|
-
.trim()
|
|
1680
|
-
.replace(/^["']|["']$/g, ""),
|
|
1681
|
-
fontSize: roundedMetric(run.fontSize * scale),
|
|
1682
|
-
bold: Number(run.fontWeight) >= 600,
|
|
1683
|
-
})),
|
|
1684
|
-
}));
|
|
1685
|
-
if (mapped.dash !== undefined) mapped.dash = powerPointDashStyle(mapped.dash);
|
|
1686
|
-
for (const key of ["fill", "stroke", "color"]) {
|
|
1687
|
-
if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
|
|
1688
|
-
}
|
|
1689
|
-
if (Array.isArray(mapped.paragraphs)) {
|
|
1690
|
-
mapped.paragraphs = mapParagraphs(mapped.paragraphs);
|
|
1691
|
-
}
|
|
1692
|
-
if (mapped.text?.paragraphs) {
|
|
1693
|
-
mapped.text = {
|
|
1694
|
-
...mapped.text,
|
|
1695
|
-
paragraphs: mapParagraphs(mapped.text.paragraphs),
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
if (mapped.textInsets) {
|
|
1699
|
-
mapped.textInsets = Object.fromEntries(
|
|
1700
|
-
Object.entries(mapped.textInsets).map(([key, value]) => [
|
|
1701
|
-
key,
|
|
1702
|
-
roundedMetric(value * scale),
|
|
1703
|
-
]),
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1706
|
-
return mapped;
|
|
1707
|
-
};
|
|
1708
|
-
const fallbacks = snapshot.fallbacks.map((fallback) => ({
|
|
1709
|
-
...fallback,
|
|
1710
|
-
path: `architecture[${blockIndex}].${fallback.path}`,
|
|
1711
|
-
...mapBounds(fallback),
|
|
1712
|
-
}));
|
|
1713
|
-
const elements = [];
|
|
1763
|
+
const fallbacks = [];
|
|
1714
1764
|
const architectureGroups = [...wrapper.querySelectorAll("[data-architecture-type]")];
|
|
1715
1765
|
const findById = (id) =>
|
|
1716
1766
|
architectureGroups.find((element) => element.getAttribute("data-architecture-id") === id);
|
|
@@ -1741,13 +1791,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1741
1791
|
width: bounds.width,
|
|
1742
1792
|
height: bounds.height,
|
|
1743
1793
|
},
|
|
1744
|
-
architecture: {
|
|
1745
|
-
kind: "icon-picture",
|
|
1746
|
-
id: icon.id,
|
|
1747
|
-
sourcePath: icon.sourcePath,
|
|
1748
|
-
order: icon.order,
|
|
1749
|
-
z: icon.z,
|
|
1750
|
-
},
|
|
1751
1794
|
});
|
|
1752
1795
|
}
|
|
1753
1796
|
}
|
|
@@ -1776,10 +1819,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1776
1819
|
width: bounds.width,
|
|
1777
1820
|
height: bounds.height,
|
|
1778
1821
|
},
|
|
1779
|
-
architecture: {
|
|
1780
|
-
...object.architecture,
|
|
1781
|
-
kind: "image-picture",
|
|
1782
|
-
},
|
|
1783
1822
|
});
|
|
1784
1823
|
}
|
|
1785
1824
|
}
|
|
@@ -1819,15 +1858,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1819
1858
|
),
|
|
1820
1859
|
);
|
|
1821
1860
|
}
|
|
1822
|
-
const foregroundElement = (layer) => ({
|
|
1823
|
-
type: "image",
|
|
1824
|
-
src: layer.src,
|
|
1825
|
-
alt: layer.alt,
|
|
1826
|
-
fit: "fill",
|
|
1827
|
-
opacity: 1,
|
|
1828
|
-
...layer.bounds,
|
|
1829
|
-
architecture: layer.architecture,
|
|
1830
|
-
});
|
|
1831
1861
|
if (!foregroundReady) {
|
|
1832
1862
|
fallbacks.push(
|
|
1833
1863
|
pptxFallback(
|
|
@@ -1841,84 +1871,25 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1841
1871
|
}
|
|
1842
1872
|
|
|
1843
1873
|
for (const sourceObject of snapshot.objects) {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
...mapBounds(sourceObject),
|
|
1847
|
-
...(sourceObject.points
|
|
1848
|
-
? {
|
|
1849
|
-
points: sourceObject.points.map((point) => ({
|
|
1850
|
-
x: roundedMetric(originX + point.x * scale),
|
|
1851
|
-
y: roundedMetric(originY + point.y * scale),
|
|
1852
|
-
})),
|
|
1853
|
-
}
|
|
1854
|
-
: {}),
|
|
1855
|
-
...(sourceObject.strokeWidth !== undefined
|
|
1856
|
-
? { strokeWidth: roundedMetric(sourceObject.strokeWidth * scale) }
|
|
1857
|
-
: {}),
|
|
1858
|
-
...(sourceObject.cornerRadius !== undefined
|
|
1859
|
-
? { cornerRadius: roundedMetric(sourceObject.cornerRadius * scale) }
|
|
1860
|
-
: {}),
|
|
1861
|
-
});
|
|
1862
|
-
if (object.type === "shape") {
|
|
1863
|
-
const opacity = Number.isFinite(object.opacity) ? object.opacity : 1;
|
|
1864
|
-
if (object.text?.paragraphs) {
|
|
1865
|
-
object.text = {
|
|
1866
|
-
...object.text,
|
|
1867
|
-
paragraphs: object.text.paragraphs.map((paragraph) => ({
|
|
1868
|
-
...paragraph,
|
|
1869
|
-
runs: paragraph.runs.map((run) => ({
|
|
1870
|
-
...run,
|
|
1871
|
-
opacity: (Number.isFinite(run.opacity) ? run.opacity : 1) * opacity,
|
|
1872
|
-
})),
|
|
1873
|
-
})),
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
if (object.type === "image") {
|
|
1878
|
-
const layer = foregroundLayers.get(`image:${object.architecture.id}`);
|
|
1874
|
+
if (sourceObject.type === "image") {
|
|
1875
|
+
const layer = foregroundLayers.get(`image:${sourceObject.architecture.id}`);
|
|
1879
1876
|
fallbacks.push({
|
|
1880
1877
|
type: "architecture-image",
|
|
1881
|
-
path: `architecture[${blockIndex}].${
|
|
1882
|
-
reason:
|
|
1878
|
+
path: `architecture[${blockIndex}].${sourceObject.architecture.sourcePath}`,
|
|
1879
|
+
reason: layer
|
|
1883
1880
|
? "architecture-image-rendered-as-foreground-picture"
|
|
1884
1881
|
: "architecture-image-rendered-as-artwork",
|
|
1885
1882
|
...mapBounds(sourceObject),
|
|
1886
|
-
...(
|
|
1883
|
+
...(layer ? { artwork: false } : {}),
|
|
1887
1884
|
});
|
|
1888
|
-
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1889
|
-
continue;
|
|
1890
1885
|
}
|
|
1891
|
-
elements.push(object);
|
|
1892
|
-
if (object.type === "shape" && object.architecture?.kind === "node" && sourceObject.icon) {
|
|
1893
|
-
const layer = foregroundLayers.get(`icon:${object.architecture.id}`);
|
|
1894
|
-
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
for (const icon of snapshot.icons || []) {
|
|
1898
|
-
const layer = foregroundLayers.get(`icon:${icon.id}`);
|
|
1899
|
-
fallbacks.push({
|
|
1900
|
-
type: "architecture-icon",
|
|
1901
|
-
path: `architecture[${blockIndex}].${icon.sourcePath}`,
|
|
1902
|
-
reason: foregroundReady && layer
|
|
1903
|
-
? "icon-rendered-as-foreground-picture"
|
|
1904
|
-
: "icon-rendered-as-artwork",
|
|
1905
|
-
icon: icon.icon,
|
|
1906
|
-
...mapBounds(icon),
|
|
1907
|
-
...(foregroundReady && layer ? { artwork: false } : {}),
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
for (const sourceObject of snapshot.objects) {
|
|
1911
1886
|
const architecture = sourceObject.architecture;
|
|
1912
1887
|
if (!architecture) continue;
|
|
1913
1888
|
if (architecture.kind === "group" || architecture.kind === "node") {
|
|
1914
1889
|
const group = findById(architecture.id);
|
|
1915
1890
|
if (!group) continue;
|
|
1916
1891
|
[...group.children]
|
|
1917
|
-
.filter((child) =>
|
|
1918
|
-
foregroundReady
|
|
1919
|
-
? child.matches("rect, ellipse, text")
|
|
1920
|
-
: child.matches("text"),
|
|
1921
|
-
)
|
|
1892
|
+
.filter((child) => child.matches("rect, ellipse, text"))
|
|
1922
1893
|
.forEach((child) => child.setAttribute("data-pptx-native", sourceObject.type));
|
|
1923
1894
|
} else if (architecture.kind === "connector") {
|
|
1924
1895
|
architectureGroups
|
|
@@ -1940,13 +1911,24 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1940
1911
|
.forEach((label) => label.setAttribute("data-pptx-native", sourceObject.type));
|
|
1941
1912
|
}
|
|
1942
1913
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
}
|
|
1914
|
+
for (const icon of snapshot.icons || []) {
|
|
1915
|
+
const layer = foregroundLayers.get(`icon:${icon.id}`);
|
|
1916
|
+
fallbacks.push({
|
|
1917
|
+
type: "architecture-icon",
|
|
1918
|
+
path: `architecture[${blockIndex}].${icon.sourcePath}`,
|
|
1919
|
+
reason: layer
|
|
1920
|
+
? "icon-rendered-as-foreground-picture"
|
|
1921
|
+
: "icon-rendered-as-artwork",
|
|
1922
|
+
icon: icon.icon,
|
|
1923
|
+
...mapBounds(icon),
|
|
1924
|
+
...(layer ? { artwork: false } : {}),
|
|
1948
1925
|
});
|
|
1949
1926
|
}
|
|
1927
|
+
foregroundCandidates.forEach((candidate) => {
|
|
1928
|
+
if (foregroundLayers.has(candidate.key)) {
|
|
1929
|
+
candidate.source.setAttribute("data-pptx-native", "image");
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1950
1932
|
if (snapshot.routing.degraded) {
|
|
1951
1933
|
fallbacks.push(
|
|
1952
1934
|
pptxFallback(
|
|
@@ -1957,7 +1939,190 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1957
1939
|
),
|
|
1958
1940
|
);
|
|
1959
1941
|
}
|
|
1960
|
-
|
|
1942
|
+
|
|
1943
|
+
const fontFace = getComputedStyle(svg).fontFamily
|
|
1944
|
+
.split(",")[0]
|
|
1945
|
+
.trim()
|
|
1946
|
+
.replace(/^["']|["']$/g, "");
|
|
1947
|
+
const { scene } = architectureSnapshotToScene(snapshot, {
|
|
1948
|
+
path: `architecture[${blockIndex}]`,
|
|
1949
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
1950
|
+
resolveDash: powerPointDashStyle,
|
|
1951
|
+
resolveImage: (entry, kind) => {
|
|
1952
|
+
const key = kind === "icon-picture"
|
|
1953
|
+
? `icon:${entry.id}`
|
|
1954
|
+
: `image:${entry.architecture?.id || entry.id}`;
|
|
1955
|
+
return foregroundLayers.get(key) || "";
|
|
1956
|
+
},
|
|
1957
|
+
fontFace,
|
|
1958
|
+
scale,
|
|
1959
|
+
originX,
|
|
1960
|
+
originY,
|
|
1961
|
+
});
|
|
1962
|
+
const mapped = sceneToPptxElements(scene, {
|
|
1963
|
+
pathPrefix: `architecture[${blockIndex}]`,
|
|
1964
|
+
emitPath: false,
|
|
1965
|
+
emitZOrder: false,
|
|
1966
|
+
});
|
|
1967
|
+
return { elements: mapped.elements, fallbacks: [...fallbacks, ...mapped.fallbacks] };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function unprefixScenePath(path, prefix) {
|
|
1971
|
+
return typeof path === "string" && path.startsWith(`${prefix}.`)
|
|
1972
|
+
? path.slice(prefix.length + 1)
|
|
1973
|
+
: path;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function mermaidEdgeLabelElement(svg, id) {
|
|
1977
|
+
if (!id) return null;
|
|
1978
|
+
const root = svg.querySelector("g.root");
|
|
1979
|
+
const labels = root ? [...root.querySelectorAll(":scope > g.edgeLabels > g.edgeLabel")] : [];
|
|
1980
|
+
return labels.find((label) => {
|
|
1981
|
+
const labelGroup = label.querySelector(":scope > g.label");
|
|
1982
|
+
return labelGroup?.getAttribute("data-id") === id;
|
|
1983
|
+
}) || null;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function mermaidElementForSourcePath(svg, sourcePath) {
|
|
1987
|
+
const root = svg.querySelector("g.root");
|
|
1988
|
+
if (!root) return sourcePath === "svg" ? svg : null;
|
|
1989
|
+
const indexed = /^(nodes|clusters|edges)\[(\d+)\]$/.exec(sourcePath || "");
|
|
1990
|
+
if (indexed) {
|
|
1991
|
+
const [, kind, rawIndex] = indexed;
|
|
1992
|
+
const index = Number(rawIndex);
|
|
1993
|
+
const selectors = {
|
|
1994
|
+
nodes: ":scope > g.nodes > g.node",
|
|
1995
|
+
clusters: ":scope > g.clusters > g.cluster",
|
|
1996
|
+
edges: ":scope > g.edgePaths > path.flowchart-link",
|
|
1997
|
+
};
|
|
1998
|
+
return [...root.querySelectorAll(selectors[kind])][index] || null;
|
|
1999
|
+
}
|
|
2000
|
+
if (sourcePath === "svg") return svg;
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function mermaidFallbackElementForBounds(svg, deck, bounds) {
|
|
2005
|
+
const ignored = new Set(["defs", "desc", "filter", "linearGradient", "marker", "metadata", "script", "style"]);
|
|
2006
|
+
const containerClasses = new Set(["root", "clusters", "edgePaths", "edgeLabels", "edgeLabel", "label", "nodes"]);
|
|
2007
|
+
const candidates = [...svg.querySelectorAll("circle, ellipse, foreignObject, g, image, line, path, polygon, polyline, rect, text, use")]
|
|
2008
|
+
.filter((candidate) => {
|
|
2009
|
+
if (candidate.closest("[data-pptx-native]")) return false;
|
|
2010
|
+
if (ignored.has(String(candidate.localName || candidate.tagName).toLowerCase())) return false;
|
|
2011
|
+
if (Array.from(candidate.classList || []).some((name) => containerClasses.has(name))) return false;
|
|
2012
|
+
const rect = candidate.getBoundingClientRect();
|
|
2013
|
+
return rect.width > 0 && rect.height > 0;
|
|
2014
|
+
});
|
|
2015
|
+
return candidates.find((candidate) => {
|
|
2016
|
+
const candidateBounds = fallbackBounds(candidate, deck);
|
|
2017
|
+
return (
|
|
2018
|
+
Math.abs(candidateBounds.x - bounds.x) <= 1 &&
|
|
2019
|
+
Math.abs(candidateBounds.y - bounds.y) <= 1 &&
|
|
2020
|
+
Math.abs(candidateBounds.width - bounds.width) <= 1 &&
|
|
2021
|
+
Math.abs(candidateBounds.height - bounds.height) <= 1
|
|
2022
|
+
);
|
|
2023
|
+
}) || null;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function markMermaidNativeElements(svg, mappedElements, pathPrefix, sourceElements) {
|
|
2027
|
+
for (const element of mappedElements) {
|
|
2028
|
+
const sourcePath = unprefixScenePath(element.path, pathPrefix);
|
|
2029
|
+
let source = sourceElements?.get(sourcePath) || mermaidElementForSourcePath(svg, sourcePath);
|
|
2030
|
+
let ownerPath = sourcePath;
|
|
2031
|
+
while (!source && sourceElements && ownerPath.includes(".")) {
|
|
2032
|
+
ownerPath = ownerPath.slice(0, ownerPath.lastIndexOf("."));
|
|
2033
|
+
source = sourceElements.get(ownerPath);
|
|
2034
|
+
}
|
|
2035
|
+
if (source && !source.hasAttribute("data-pptx-native")) {
|
|
2036
|
+
const nativeKind = element.type === "shape" ? "shape" : element.type;
|
|
2037
|
+
source.setAttribute("data-pptx-native", nativeKind);
|
|
2038
|
+
}
|
|
2039
|
+
if (!sourceElements && element.type === "connector") {
|
|
2040
|
+
const id = element.mermaid?.id || source?.getAttribute("data-id") || source?.getAttribute("id") || "";
|
|
2041
|
+
mermaidEdgeLabelElement(svg, id)?.setAttribute("data-pptx-native", "text");
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function mermaidWholeElementFallbackRequired(scene, diagnostics) {
|
|
2047
|
+
const nodes = Array.isArray(scene?.nodes) ? scene.nodes : [];
|
|
2048
|
+
const reason = nodes[0]?.reason || diagnostics.find((entry) => entry?.reason)?.reason || "";
|
|
2049
|
+
return (
|
|
2050
|
+
nodes.length === 1 &&
|
|
2051
|
+
nodes[0]?.kind === "fallback" &&
|
|
2052
|
+
nodes[0]?.sourcePath === "svg" &&
|
|
2053
|
+
(
|
|
2054
|
+
reason.startsWith("mermaid-scene-adapter-failed:") ||
|
|
2055
|
+
reason.startsWith("mermaid-scene-limit-exceeded:") ||
|
|
2056
|
+
reason === "unsupported-mermaid-svg-structure"
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function collectMermaidObjects(element, deck, blockIndex) {
|
|
2062
|
+
const svg = element.querySelector("svg");
|
|
2063
|
+
if (!svg) {
|
|
2064
|
+
return {
|
|
2065
|
+
elements: [],
|
|
2066
|
+
fallbacks: [pptxFallback("mermaid", element, deck, "mermaid-rendered-as-artwork")],
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const pathPrefix = `mermaid[${blockIndex}]`;
|
|
2070
|
+
const { scene, diagnostics, sourceElements } = mermaidSvgToScene(svg, {
|
|
2071
|
+
path: pathPrefix,
|
|
2072
|
+
deck,
|
|
2073
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
2074
|
+
includeSourceElements: true,
|
|
2075
|
+
});
|
|
2076
|
+
if (mermaidWholeElementFallbackRequired(scene, diagnostics)) {
|
|
2077
|
+
return {
|
|
2078
|
+
elements: [],
|
|
2079
|
+
fallbacks: [pptxFallback("mermaid", element, deck, "mermaid-rendered-as-artwork")],
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const mapped = sceneToPptxElements(scene, {
|
|
2083
|
+
pathPrefix,
|
|
2084
|
+
groupPreset: "rect",
|
|
2085
|
+
zOrderBase: Number(element.dataset.pptxZOrder),
|
|
2086
|
+
});
|
|
2087
|
+
markMermaidNativeElements(svg, mapped.elements, pathPrefix, sourceElements);
|
|
2088
|
+
const fallbackNodes = new Map(
|
|
2089
|
+
scene.nodes
|
|
2090
|
+
.filter((node) => node.kind === "fallback")
|
|
2091
|
+
.map((node) => [node.sourcePath, node]),
|
|
2092
|
+
);
|
|
2093
|
+
const fallbacks = mapped.fallbacks.map((fallback) => {
|
|
2094
|
+
const sourcePath = fallback.sourcePath || unprefixScenePath(fallback.path, pathPrefix);
|
|
2095
|
+
const node = fallbackNodes.get(sourcePath);
|
|
2096
|
+
const source =
|
|
2097
|
+
sourceElements?.get(sourcePath) ||
|
|
2098
|
+
mermaidElementForSourcePath(svg, sourcePath) ||
|
|
2099
|
+
mermaidFallbackElementForBounds(svg, deck, fallback) ||
|
|
2100
|
+
svg;
|
|
2101
|
+
const sourceBounds = source.getBoundingClientRect();
|
|
2102
|
+
const padding = ["path", "line", "polyline"].includes(source.localName)
|
|
2103
|
+
? Math.max(1, (fallback.width - sourceBounds.width) / 2, (fallback.height - sourceBounds.height) / 2)
|
|
2104
|
+
: 0;
|
|
2105
|
+
const captured = pptxFallback("mermaid", source, deck, fallback.reason, {
|
|
2106
|
+
captureElement: source,
|
|
2107
|
+
includeDescendants: true,
|
|
2108
|
+
artwork: fallback.artwork,
|
|
2109
|
+
padding,
|
|
2110
|
+
});
|
|
2111
|
+
return {
|
|
2112
|
+
...captured,
|
|
2113
|
+
path: fallback.path,
|
|
2114
|
+
sourcePath,
|
|
2115
|
+
reason: fallback.reason,
|
|
2116
|
+
x: fallback.x,
|
|
2117
|
+
y: fallback.y,
|
|
2118
|
+
width: fallback.width,
|
|
2119
|
+
height: fallback.height,
|
|
2120
|
+
zOrder: fallback.zOrder,
|
|
2121
|
+
...(node?.id ? { id: node.id } : {}),
|
|
2122
|
+
...(fallback.artwork === false ? { artwork: false } : {}),
|
|
2123
|
+
};
|
|
2124
|
+
});
|
|
2125
|
+
return { elements: mapped.elements, fallbacks };
|
|
1961
2126
|
}
|
|
1962
2127
|
|
|
1963
2128
|
async function collectPptxSlide(slide, index) {
|
|
@@ -2064,10 +2229,17 @@ async function collectPptxSlide(slide, index) {
|
|
|
2064
2229
|
});
|
|
2065
2230
|
}
|
|
2066
2231
|
});
|
|
2067
|
-
deck.querySelectorAll("pre.mermaid").
|
|
2232
|
+
for (const [blockIndex, element] of [...deck.querySelectorAll("pre.mermaid")].entries()) {
|
|
2068
2233
|
const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
2234
|
+
if (covered) continue;
|
|
2235
|
+
try {
|
|
2236
|
+
const mermaid = collectMermaidObjects(element, deck, blockIndex);
|
|
2237
|
+
elements.push(...mermaid.elements);
|
|
2238
|
+
fallbacks.push(...mermaid.fallbacks);
|
|
2239
|
+
} catch (_) {
|
|
2240
|
+
addFallback("mermaid", element, "mermaid-rendered-as-artwork");
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2071
2243
|
|
|
2072
2244
|
const insideFallback = (element) =>
|
|
2073
2245
|
[...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
@@ -2079,6 +2251,7 @@ async function collectPptxSlide(slide, index) {
|
|
|
2079
2251
|
(element) =>
|
|
2080
2252
|
!insideFallback(element) &&
|
|
2081
2253
|
!element.closest(".architecture-diagram") &&
|
|
2254
|
+
!element.closest("pre.mermaid, .mermaid") &&
|
|
2082
2255
|
!element.closest("table") &&
|
|
2083
2256
|
!(element.matches("p") && element.closest("blockquote, li")),
|
|
2084
2257
|
);
|
|
@@ -2475,7 +2648,7 @@ async function renderPptxDeck(
|
|
|
2475
2648
|
}
|
|
2476
2649
|
const token = ++renderToken;
|
|
2477
2650
|
for (const slide of rendered) {
|
|
2478
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2651
|
+
await runMermaid(slide.bodyEl, slide.deck, token, false);
|
|
2479
2652
|
}
|
|
2480
2653
|
await waitForImages(stage);
|
|
2481
2654
|
await afterLayout();
|
|
@@ -2590,7 +2763,7 @@ async function renderPrintDeck(
|
|
|
2590
2763
|
}
|
|
2591
2764
|
}
|
|
2592
2765
|
for (const slide of rendered) {
|
|
2593
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2766
|
+
await runMermaid(slide.bodyEl, slide.deck, renderToken, false);
|
|
2594
2767
|
}
|
|
2595
2768
|
await waitForImages(stage);
|
|
2596
2769
|
await afterLayout();
|
|
@@ -2669,7 +2842,7 @@ async function renderCaptureSlide(
|
|
|
2669
2842
|
}
|
|
2670
2843
|
|
|
2671
2844
|
const token = ++renderToken;
|
|
2672
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2845
|
+
await runMermaid(slide.bodyEl, slide.deck, token, false);
|
|
2673
2846
|
await waitForImages(stage);
|
|
2674
2847
|
await afterLayout();
|
|
2675
2848
|
|
|
@@ -2790,8 +2963,10 @@ let pptxExportAvailable = false;
|
|
|
2790
2963
|
let markdownImportAvailable = false;
|
|
2791
2964
|
let presenterViewOpen = false;
|
|
2792
2965
|
let presenterViewRequested = false;
|
|
2793
|
-
let
|
|
2794
|
-
let
|
|
2966
|
+
let exportPending = false;
|
|
2967
|
+
let exportNotificationTimer = null;
|
|
2968
|
+
let exportNotificationRemaining = 0;
|
|
2969
|
+
let exportNotificationStarted = 0;
|
|
2795
2970
|
|
|
2796
2971
|
// Derive a short overview title from a slide fragment: first heading, else first
|
|
2797
2972
|
// non-empty body line, trimmed. Mirrors the skill's title rule.
|
|
@@ -2846,6 +3021,7 @@ async function fetchDeck() {
|
|
|
2846
3021
|
function setArchitectureEditMode(enabled) {
|
|
2847
3022
|
const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
|
|
2848
3023
|
if (next === architectureEditMode) return false;
|
|
3024
|
+
if (next && fixedPreviewMode) setFixedPreviewMode(false);
|
|
2849
3025
|
architectureEditMode = next;
|
|
2850
3026
|
document.body.classList.toggle("architecture-edit-mode", next);
|
|
2851
3027
|
updateArchitectureEditButton(next);
|
|
@@ -3450,93 +3626,109 @@ function updatePresenterButton(running, message = "") {
|
|
|
3450
3626
|
syncMoreControls();
|
|
3451
3627
|
}
|
|
3452
3628
|
|
|
3453
|
-
|
|
3454
|
-
if (
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3629
|
+
function pauseExportNotification() {
|
|
3630
|
+
if (exportNotificationTimer === null) return;
|
|
3631
|
+
clearTimeout(exportNotificationTimer);
|
|
3632
|
+
exportNotificationTimer = null;
|
|
3633
|
+
exportNotificationRemaining = Math.max(
|
|
3634
|
+
0,
|
|
3635
|
+
exportNotificationRemaining - (performance.now() - exportNotificationStarted),
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3462
3638
|
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
const message = `Saved ${filename}.`;
|
|
3475
|
-
if (status) status.textContent = message;
|
|
3476
|
-
if (button) {
|
|
3477
|
-
button.dataset.state = "active";
|
|
3478
|
-
button.title = message;
|
|
3479
|
-
}
|
|
3480
|
-
syncMoreControls();
|
|
3481
|
-
} catch (error) {
|
|
3482
|
-
const message = error?.message || "Could not save the PDF.";
|
|
3483
|
-
console.error("PDF export failed", error);
|
|
3484
|
-
if (status) status.textContent = message;
|
|
3485
|
-
if (button) {
|
|
3486
|
-
button.dataset.state = "error";
|
|
3487
|
-
button.title = message;
|
|
3488
|
-
}
|
|
3489
|
-
syncMoreControls();
|
|
3490
|
-
} finally {
|
|
3491
|
-
pdfExportPending = false;
|
|
3492
|
-
if (button) button.disabled = false;
|
|
3493
|
-
if (pptxButton) pptxButton.disabled = false;
|
|
3639
|
+
function dismissExportNotification() {
|
|
3640
|
+
const notification = document.getElementById("exportNotification");
|
|
3641
|
+
const restoreFocus = notification.contains(document.activeElement);
|
|
3642
|
+
pauseExportNotification();
|
|
3643
|
+
exportNotificationRemaining = 0;
|
|
3644
|
+
notification.hidden = true;
|
|
3645
|
+
document.getElementById("exportStatus").textContent = "";
|
|
3646
|
+
document.getElementById("exportErrorStatus").textContent = "";
|
|
3647
|
+
if (restoreFocus) {
|
|
3648
|
+
const more = document.getElementById("navMore");
|
|
3649
|
+
(more.getClientRects().length ? more : document.body).focus();
|
|
3494
3650
|
}
|
|
3495
3651
|
}
|
|
3496
3652
|
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3653
|
+
function resumeExportNotification() {
|
|
3654
|
+
const notification = document.getElementById("exportNotification");
|
|
3655
|
+
if (
|
|
3656
|
+
notification.hidden ||
|
|
3657
|
+
notification.dataset.state !== "success" ||
|
|
3658
|
+
notification.matches(":hover, :focus-within") ||
|
|
3659
|
+
exportNotificationTimer !== null
|
|
3660
|
+
) {
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
exportNotificationStarted = performance.now();
|
|
3664
|
+
exportNotificationTimer = setTimeout(dismissExportNotification, exportNotificationRemaining);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function showExportNotification(state, message, path = "") {
|
|
3668
|
+
pauseExportNotification();
|
|
3669
|
+
const notification = document.getElementById("exportNotification");
|
|
3670
|
+
const location = document.getElementById("exportNotificationPath");
|
|
3671
|
+
notification.dataset.state = state;
|
|
3672
|
+
document.getElementById("exportNotificationMessage").textContent = message;
|
|
3673
|
+
location.textContent = path ? `Saved to: ${path}` : "";
|
|
3674
|
+
location.hidden = !path;
|
|
3675
|
+
document.getElementById("exportNotificationClose").hidden = state === "pending";
|
|
3676
|
+
notification.hidden = false;
|
|
3677
|
+
const announcement = path ? `${message} Saved to: ${path}` : message;
|
|
3678
|
+
document.getElementById("exportStatus").textContent = state === "error" ? "" : announcement;
|
|
3679
|
+
document.getElementById("exportErrorStatus").textContent = state === "error" ? announcement : "";
|
|
3680
|
+
exportNotificationRemaining = state === "success" ? 8000 : 0;
|
|
3681
|
+
resumeExportNotification();
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
async function exportFromCanvas(format) {
|
|
3685
|
+
const isPdf = format === "pdf";
|
|
3686
|
+
if (exportPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3687
|
+
exportPending = true;
|
|
3688
|
+
const label = isPdf ? "PDF" : "PowerPoint";
|
|
3501
3689
|
const pdfButton = document.getElementById("navExport");
|
|
3502
|
-
const
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3690
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
3691
|
+
const button = isPdf ? pdfButton : pptxButton;
|
|
3692
|
+
const idleTitle = button.title;
|
|
3693
|
+
pdfButton.disabled = true;
|
|
3694
|
+
pptxButton.disabled = true;
|
|
3695
|
+
delete button.dataset.state;
|
|
3696
|
+
button.title = `Saving ${label}.`;
|
|
3697
|
+
showExportNotification("pending", `Saving ${label}...`);
|
|
3698
|
+
syncMoreControls();
|
|
3506
3699
|
|
|
3507
3700
|
try {
|
|
3508
|
-
const response = await fetch("./export-pptx", {
|
|
3701
|
+
const response = await fetch(isPdf ? "./export" : "./export-pptx", {
|
|
3509
3702
|
method: "POST",
|
|
3510
3703
|
headers: { Accept: "application/json" },
|
|
3511
3704
|
cache: "no-store",
|
|
3512
3705
|
});
|
|
3513
|
-
const data = await response.json()
|
|
3514
|
-
if (!response.ok) {
|
|
3515
|
-
throw new Error(
|
|
3706
|
+
const data = await response.json();
|
|
3707
|
+
if (!response.ok || data?.ok !== true) {
|
|
3708
|
+
throw new Error(
|
|
3709
|
+
(typeof data?.message === "string" && data.message) ||
|
|
3710
|
+
`${label} export failed (${response.status}).`,
|
|
3711
|
+
);
|
|
3516
3712
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
data.fallbackCount > 0 ? ` ${data.fallbackCount} fallback item(s) preserved.` : "";
|
|
3520
|
-
const message = `Saved ${filename}.${fallback}`;
|
|
3521
|
-
if (status) status.textContent = message;
|
|
3522
|
-
if (button) {
|
|
3523
|
-
button.dataset.state = "active";
|
|
3524
|
-
button.title = message;
|
|
3713
|
+
if (typeof data.path !== "string" || !data.path.trim() || !data.path.split(/[\\/]/).pop()) {
|
|
3714
|
+
throw new Error(`${label} export returned an invalid save location.`);
|
|
3525
3715
|
}
|
|
3526
|
-
|
|
3716
|
+
const filename = data.path.split(/[\\/]/).pop();
|
|
3717
|
+
const fallback =
|
|
3718
|
+
!isPdf && data.fallbackCount > 0 ? ` ${data.fallbackCount} fallback item(s) preserved.` : "";
|
|
3719
|
+
const message = `${label} saved: ${filename}.${fallback}`;
|
|
3720
|
+
showExportNotification("success", message, data.path);
|
|
3527
3721
|
} catch (error) {
|
|
3528
|
-
const message = error?.message || "
|
|
3529
|
-
console.error(
|
|
3530
|
-
|
|
3531
|
-
if (button) {
|
|
3532
|
-
button.dataset.state = "error";
|
|
3533
|
-
button.title = message;
|
|
3534
|
-
}
|
|
3535
|
-
syncMoreControls();
|
|
3722
|
+
const message = `Could not save ${label}. ${error?.message || "Export failed."}`;
|
|
3723
|
+
console.error(`${label} export failed`, error);
|
|
3724
|
+
showExportNotification("error", message);
|
|
3536
3725
|
} finally {
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3726
|
+
exportPending = false;
|
|
3727
|
+
pdfButton.disabled = false;
|
|
3728
|
+
pptxButton.disabled = false;
|
|
3729
|
+
delete button.dataset.state;
|
|
3730
|
+
button.title = idleTitle;
|
|
3731
|
+
syncMoreControls();
|
|
3540
3732
|
}
|
|
3541
3733
|
}
|
|
3542
3734
|
|
|
@@ -3982,8 +4174,8 @@ function wireControls() {
|
|
|
3982
4174
|
bind("navPresent", openPresenterWindow, { closeMore: true });
|
|
3983
4175
|
bind("navPresenterView", openPresenterView, { closeMore: true });
|
|
3984
4176
|
bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
|
|
3985
|
-
bind("navExport",
|
|
3986
|
-
bind("navExportPptx",
|
|
4177
|
+
bind("navExport", () => exportFromCanvas("pdf"), { closeMore: true });
|
|
4178
|
+
bind("navExportPptx", () => exportFromCanvas("pptx"), { closeMore: true });
|
|
3987
4179
|
bind("navImport", toggleImportPicker, { closeMore: true });
|
|
3988
4180
|
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
3989
4181
|
bind("overviewClose", closeOverview);
|
|
@@ -3995,6 +4187,13 @@ function wireControls() {
|
|
|
3995
4187
|
bind("presenterToggleButton", togglePresenterWindow);
|
|
3996
4188
|
bind("presenterReturnButton", closePresenterView);
|
|
3997
4189
|
|
|
4190
|
+
const exportNotification = document.getElementById("exportNotification");
|
|
4191
|
+
document.getElementById("exportNotificationClose").addEventListener("click", dismissExportNotification);
|
|
4192
|
+
exportNotification.addEventListener("pointerenter", pauseExportNotification);
|
|
4193
|
+
exportNotification.addEventListener("pointerleave", resumeExportNotification);
|
|
4194
|
+
exportNotification.addEventListener("focusin", pauseExportNotification);
|
|
4195
|
+
exportNotification.addEventListener("focusout", () => queueMicrotask(resumeExportNotification));
|
|
4196
|
+
|
|
3998
4197
|
const importFilter = document.getElementById("importFilter");
|
|
3999
4198
|
if (importFilter) {
|
|
4000
4199
|
importFilter.addEventListener("input", renderImportList);
|
|
@@ -4155,6 +4354,17 @@ function init() {
|
|
|
4155
4354
|
requestArchitectureEditMode(true);
|
|
4156
4355
|
}
|
|
4157
4356
|
|
|
4357
|
+
// Canvas and CLI preview start on the fixed 16:9 output surface. Presenter
|
|
4358
|
+
// views keep their purpose-built layouts, and the control still lets users
|
|
4359
|
+
// switch back to the responsive canvas layout.
|
|
4360
|
+
if (
|
|
4361
|
+
!presenterMode &&
|
|
4362
|
+
!presenterViewRequested &&
|
|
4363
|
+
params.get("responsive") !== "1"
|
|
4364
|
+
) {
|
|
4365
|
+
setFixedPreviewMode(true);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4158
4368
|
updateArchitectureEditButton();
|
|
4159
4369
|
if (!previewMode) wireControls();
|
|
4160
4370
|
else if (navigationEnabled) {
|