@markdstage/markdstage 3.1.0 → 3.3.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/package.json +1 -1
- package/shared/README.md +61 -9
- package/shared/architecture-editor/editor.js +1 -1
- package/shared/architecture-reference.mjs +150 -0
- package/shared/architecture-validation.mjs +309 -0
- package/shared/markdstage-guide.mjs +62 -142
- package/shared/renderer/architecture-contract.mjs +13707 -0
- package/shared/renderer/architecture-diagnostics.mjs +426 -0
- package/shared/renderer/architecture-scene.mjs +312 -0
- package/shared/renderer/architecture.mjs +221 -217
- package/shared/renderer/index.html +9 -1
- package/shared/renderer/mermaid-scene.mjs +1248 -0
- package/shared/renderer/renderer.js +429 -232
- 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 +5 -0
- package/shared/runtime/architecture-source.mjs +9 -1
- package/shared/runtime/pptx-package.mjs +4 -3
- package/shared/schema/README.md +60 -5
- package/shared/schema/architecture-contract.mjs +355 -0
- package/shared/scripts/generate-architecture-contract.mjs +41 -0
- package/src/commands/validate.mjs +39 -3
- package/src/runtime.mjs +4 -0
- package/src/skills.mjs +6 -2
|
@@ -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.
|
|
@@ -564,9 +564,12 @@ function applySyntaxHighlighting(root) {
|
|
|
564
564
|
// --- mermaid ---------------------------------------------------------------
|
|
565
565
|
// Render every <pre class="mermaid"> in `scope` to SVG. Resilient: a slide with
|
|
566
566
|
// 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
|
-
|
|
567
|
+
// slide blank, so the body is always revealed in the end. Mermaid's palette is
|
|
568
|
+
// derived from the rendered deck's theme custom properties (see
|
|
569
|
+
// theme.mjs#mermaidThemeVariables) so diagrams match the slide instead of
|
|
570
|
+
// picking the closest built-in Mermaid theme, and it is re-initialized only
|
|
571
|
+
// when the resolved colors actually change.
|
|
572
|
+
function runMermaid(scope, deckEl, token, revealWhenDone = true) {
|
|
570
573
|
// Only the latest render may lift the loading veil; a stale finish is ignored.
|
|
571
574
|
const reveal = () => {
|
|
572
575
|
if (revealWhenDone && token === renderToken) {
|
|
@@ -579,13 +582,30 @@ function runMermaid(scope, theme, token, revealWhenDone = true) {
|
|
|
579
582
|
return Promise.resolve();
|
|
580
583
|
}
|
|
581
584
|
try {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
585
|
+
const themeVariables = mermaidThemeVariables(getComputedStyle(deckEl));
|
|
586
|
+
const serializedThemeVariables = JSON.stringify(themeVariables);
|
|
587
|
+
if (serializedThemeVariables !== lastMermaidThemeVariables) {
|
|
588
|
+
window.mermaid.initialize({
|
|
589
|
+
startOnLoad: false,
|
|
590
|
+
theme: "base",
|
|
591
|
+
themeVariables,
|
|
592
|
+
securityLevel: "strict",
|
|
593
|
+
});
|
|
594
|
+
lastMermaidThemeVariables = serializedThemeVariables;
|
|
586
595
|
}
|
|
587
596
|
return Promise.resolve(window.mermaid.run({ nodes }))
|
|
588
597
|
.catch((e) => console.error("Mermaid render failed", e))
|
|
598
|
+
.then(() => {
|
|
599
|
+
for (const [index, host] of [...nodes].entries()) {
|
|
600
|
+
const source = host.querySelector("svg");
|
|
601
|
+
if (!source || source.hasAttribute("data-scene-backend")) continue;
|
|
602
|
+
try {
|
|
603
|
+
renderMermaidScene(source, deckEl, index);
|
|
604
|
+
} catch (e) {
|
|
605
|
+
console.error("Mermaid scene render failed", e);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
})
|
|
589
609
|
.finally(reveal);
|
|
590
610
|
} catch (e) {
|
|
591
611
|
console.error("Mermaid init failed", e);
|
|
@@ -594,6 +614,79 @@ function runMermaid(scope, theme, token, revealWhenDone = true) {
|
|
|
594
614
|
}
|
|
595
615
|
}
|
|
596
616
|
|
|
617
|
+
function renderMermaidScene(svg, deck, blockIndex) {
|
|
618
|
+
const result = mermaidSvgToScene(svg, {
|
|
619
|
+
path: `mermaid[${blockIndex}]`,
|
|
620
|
+
deck,
|
|
621
|
+
includeSourceElements: true,
|
|
622
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
623
|
+
});
|
|
624
|
+
let { scene } = result;
|
|
625
|
+
const computedStyle = (element) => {
|
|
626
|
+
const style = getComputedStyle(element);
|
|
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
|
+
};
|
|
637
|
+
const slots = new Map();
|
|
638
|
+
try {
|
|
639
|
+
for (const [index, node] of scene.nodes.entries()) {
|
|
640
|
+
const exactSource = result.sourceElements?.get(node.sourcePath) ||
|
|
641
|
+
mermaidElementForSourcePath(svg, node.sourcePath);
|
|
642
|
+
const owner = scene.nodes.find((candidate) => {
|
|
643
|
+
if (candidate === node || !node.sourcePath.startsWith(`${candidate.sourcePath}.`)) return false;
|
|
644
|
+
const candidateSource = result.sourceElements?.get(candidate.sourcePath) ||
|
|
645
|
+
mermaidElementForSourcePath(svg, candidate.sourcePath);
|
|
646
|
+
return candidateSource && (!exactSource || candidateSource.contains(exactSource));
|
|
647
|
+
});
|
|
648
|
+
if (owner) {
|
|
649
|
+
node.meta = { ...node.meta, svgOwner: owner.sourcePath };
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const source = exactSource ||
|
|
653
|
+
mermaidFallbackElementForBounds(svg, deck, node.bounds);
|
|
654
|
+
if (!source) throw new Error(`Mermaid SVG source unavailable: ${node.sourcePath}`);
|
|
655
|
+
if (slots.has(source)) {
|
|
656
|
+
node.meta = { ...node.meta, svgOwner: slots.get(source) };
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
node.meta = { ...node.meta, svg: captureSvgTree(source, { computedStyle }) };
|
|
660
|
+
slots.set(source, index);
|
|
661
|
+
}
|
|
662
|
+
for (const [source, index] of slots) {
|
|
663
|
+
const ancestor = [...slots.keys()].find((candidate) => candidate !== source && candidate.contains(source));
|
|
664
|
+
if (ancestor) {
|
|
665
|
+
scene.nodes[index].meta.svgOwner = slots.get(ancestor);
|
|
666
|
+
slots.delete(source);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
// A new Mermaid structure must stay visible even if its source mapping is
|
|
671
|
+
// not yet understood. Preserve the full safe artwork and report the reason.
|
|
672
|
+
const reason = `mermaid-svg-source-fallback: ${error.message}`;
|
|
673
|
+
console.warn(reason);
|
|
674
|
+
scene = { ...scene, nodes: [{
|
|
675
|
+
kind: "fallback", sourcePath: "svg", z: 0,
|
|
676
|
+
bounds: { x: 0, y: 0, width: scene.width, height: scene.height },
|
|
677
|
+
capability: { pptx: "fallback", reason }, reason,
|
|
678
|
+
meta: { svg: captureSvgTree(svg, { computedStyle }) },
|
|
679
|
+
}] };
|
|
680
|
+
slots.clear();
|
|
681
|
+
slots.set(svg, 0);
|
|
682
|
+
}
|
|
683
|
+
const template = slots.has(svg)
|
|
684
|
+
? { sceneNode: slots.get(svg) }
|
|
685
|
+
: captureSvgTree(svg, { slots, computedStyle });
|
|
686
|
+
scene.meta = { ...scene.meta, svgRoot: template };
|
|
687
|
+
svg.replaceWith(sceneToSvg(scene, { document, template }));
|
|
688
|
+
}
|
|
689
|
+
|
|
597
690
|
// --- slide rendering -------------------------------------------------------
|
|
598
691
|
function moveLeadingSlideTitle(header, bodyEl, specialLayout) {
|
|
599
692
|
if (specialLayout) return null;
|
|
@@ -826,7 +919,7 @@ function renderSlide(markdown) {
|
|
|
826
919
|
const images = waitForImages(slide.deck).then(() => {
|
|
827
920
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
828
921
|
});
|
|
829
|
-
const mermaid = runMermaid(slide.bodyEl, slide.
|
|
922
|
+
const mermaid = runMermaid(slide.bodyEl, slide.deck, token, false).finally(() => {
|
|
830
923
|
if (token === renderToken) scheduleLayoutRefresh();
|
|
831
924
|
});
|
|
832
925
|
Promise.all([mermaid, images]).finally(() => {
|
|
@@ -1666,51 +1759,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1666
1759
|
width: roundedMetric(object.width * scale),
|
|
1667
1760
|
height: roundedMetric(object.height * scale),
|
|
1668
1761
|
});
|
|
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 = [];
|
|
1762
|
+
const fallbacks = [];
|
|
1714
1763
|
const architectureGroups = [...wrapper.querySelectorAll("[data-architecture-type]")];
|
|
1715
1764
|
const findById = (id) =>
|
|
1716
1765
|
architectureGroups.find((element) => element.getAttribute("data-architecture-id") === id);
|
|
@@ -1741,13 +1790,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1741
1790
|
width: bounds.width,
|
|
1742
1791
|
height: bounds.height,
|
|
1743
1792
|
},
|
|
1744
|
-
architecture: {
|
|
1745
|
-
kind: "icon-picture",
|
|
1746
|
-
id: icon.id,
|
|
1747
|
-
sourcePath: icon.sourcePath,
|
|
1748
|
-
order: icon.order,
|
|
1749
|
-
z: icon.z,
|
|
1750
|
-
},
|
|
1751
1793
|
});
|
|
1752
1794
|
}
|
|
1753
1795
|
}
|
|
@@ -1776,10 +1818,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1776
1818
|
width: bounds.width,
|
|
1777
1819
|
height: bounds.height,
|
|
1778
1820
|
},
|
|
1779
|
-
architecture: {
|
|
1780
|
-
...object.architecture,
|
|
1781
|
-
kind: "image-picture",
|
|
1782
|
-
},
|
|
1783
1821
|
});
|
|
1784
1822
|
}
|
|
1785
1823
|
}
|
|
@@ -1819,15 +1857,6 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1819
1857
|
),
|
|
1820
1858
|
);
|
|
1821
1859
|
}
|
|
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
1860
|
if (!foregroundReady) {
|
|
1832
1861
|
fallbacks.push(
|
|
1833
1862
|
pptxFallback(
|
|
@@ -1841,84 +1870,25 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1841
1870
|
}
|
|
1842
1871
|
|
|
1843
1872
|
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}`);
|
|
1873
|
+
if (sourceObject.type === "image") {
|
|
1874
|
+
const layer = foregroundLayers.get(`image:${sourceObject.architecture.id}`);
|
|
1879
1875
|
fallbacks.push({
|
|
1880
1876
|
type: "architecture-image",
|
|
1881
|
-
path: `architecture[${blockIndex}].${
|
|
1882
|
-
reason:
|
|
1877
|
+
path: `architecture[${blockIndex}].${sourceObject.architecture.sourcePath}`,
|
|
1878
|
+
reason: layer
|
|
1883
1879
|
? "architecture-image-rendered-as-foreground-picture"
|
|
1884
1880
|
: "architecture-image-rendered-as-artwork",
|
|
1885
1881
|
...mapBounds(sourceObject),
|
|
1886
|
-
...(
|
|
1882
|
+
...(layer ? { artwork: false } : {}),
|
|
1887
1883
|
});
|
|
1888
|
-
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1889
|
-
continue;
|
|
1890
|
-
}
|
|
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
1884
|
}
|
|
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
1885
|
const architecture = sourceObject.architecture;
|
|
1912
1886
|
if (!architecture) continue;
|
|
1913
1887
|
if (architecture.kind === "group" || architecture.kind === "node") {
|
|
1914
1888
|
const group = findById(architecture.id);
|
|
1915
1889
|
if (!group) continue;
|
|
1916
1890
|
[...group.children]
|
|
1917
|
-
.filter((child) =>
|
|
1918
|
-
foregroundReady
|
|
1919
|
-
? child.matches("rect, ellipse, text")
|
|
1920
|
-
: child.matches("text"),
|
|
1921
|
-
)
|
|
1891
|
+
.filter((child) => child.matches("rect, ellipse, text"))
|
|
1922
1892
|
.forEach((child) => child.setAttribute("data-pptx-native", sourceObject.type));
|
|
1923
1893
|
} else if (architecture.kind === "connector") {
|
|
1924
1894
|
architectureGroups
|
|
@@ -1940,13 +1910,24 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1940
1910
|
.forEach((label) => label.setAttribute("data-pptx-native", sourceObject.type));
|
|
1941
1911
|
}
|
|
1942
1912
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
}
|
|
1913
|
+
for (const icon of snapshot.icons || []) {
|
|
1914
|
+
const layer = foregroundLayers.get(`icon:${icon.id}`);
|
|
1915
|
+
fallbacks.push({
|
|
1916
|
+
type: "architecture-icon",
|
|
1917
|
+
path: `architecture[${blockIndex}].${icon.sourcePath}`,
|
|
1918
|
+
reason: layer
|
|
1919
|
+
? "icon-rendered-as-foreground-picture"
|
|
1920
|
+
: "icon-rendered-as-artwork",
|
|
1921
|
+
icon: icon.icon,
|
|
1922
|
+
...mapBounds(icon),
|
|
1923
|
+
...(layer ? { artwork: false } : {}),
|
|
1948
1924
|
});
|
|
1949
1925
|
}
|
|
1926
|
+
foregroundCandidates.forEach((candidate) => {
|
|
1927
|
+
if (foregroundLayers.has(candidate.key)) {
|
|
1928
|
+
candidate.source.setAttribute("data-pptx-native", "image");
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1950
1931
|
if (snapshot.routing.degraded) {
|
|
1951
1932
|
fallbacks.push(
|
|
1952
1933
|
pptxFallback(
|
|
@@ -1957,7 +1938,190 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1957
1938
|
),
|
|
1958
1939
|
);
|
|
1959
1940
|
}
|
|
1960
|
-
|
|
1941
|
+
|
|
1942
|
+
const fontFace = getComputedStyle(svg).fontFamily
|
|
1943
|
+
.split(",")[0]
|
|
1944
|
+
.trim()
|
|
1945
|
+
.replace(/^["']|["']$/g, "");
|
|
1946
|
+
const { scene } = architectureSnapshotToScene(snapshot, {
|
|
1947
|
+
path: `architecture[${blockIndex}]`,
|
|
1948
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
1949
|
+
resolveDash: powerPointDashStyle,
|
|
1950
|
+
resolveImage: (entry, kind) => {
|
|
1951
|
+
const key = kind === "icon-picture"
|
|
1952
|
+
? `icon:${entry.id}`
|
|
1953
|
+
: `image:${entry.architecture?.id || entry.id}`;
|
|
1954
|
+
return foregroundLayers.get(key) || "";
|
|
1955
|
+
},
|
|
1956
|
+
fontFace,
|
|
1957
|
+
scale,
|
|
1958
|
+
originX,
|
|
1959
|
+
originY,
|
|
1960
|
+
});
|
|
1961
|
+
const mapped = sceneToPptxElements(scene, {
|
|
1962
|
+
pathPrefix: `architecture[${blockIndex}]`,
|
|
1963
|
+
emitPath: false,
|
|
1964
|
+
emitZOrder: false,
|
|
1965
|
+
});
|
|
1966
|
+
return { elements: mapped.elements, fallbacks: [...fallbacks, ...mapped.fallbacks] };
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function unprefixScenePath(path, prefix) {
|
|
1970
|
+
return typeof path === "string" && path.startsWith(`${prefix}.`)
|
|
1971
|
+
? path.slice(prefix.length + 1)
|
|
1972
|
+
: path;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function mermaidEdgeLabelElement(svg, id) {
|
|
1976
|
+
if (!id) return null;
|
|
1977
|
+
const root = svg.querySelector("g.root");
|
|
1978
|
+
const labels = root ? [...root.querySelectorAll(":scope > g.edgeLabels > g.edgeLabel")] : [];
|
|
1979
|
+
return labels.find((label) => {
|
|
1980
|
+
const labelGroup = label.querySelector(":scope > g.label");
|
|
1981
|
+
return labelGroup?.getAttribute("data-id") === id;
|
|
1982
|
+
}) || null;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function mermaidElementForSourcePath(svg, sourcePath) {
|
|
1986
|
+
const root = svg.querySelector("g.root");
|
|
1987
|
+
if (!root) return sourcePath === "svg" ? svg : null;
|
|
1988
|
+
const indexed = /^(nodes|clusters|edges)\[(\d+)\]$/.exec(sourcePath || "");
|
|
1989
|
+
if (indexed) {
|
|
1990
|
+
const [, kind, rawIndex] = indexed;
|
|
1991
|
+
const index = Number(rawIndex);
|
|
1992
|
+
const selectors = {
|
|
1993
|
+
nodes: ":scope > g.nodes > g.node",
|
|
1994
|
+
clusters: ":scope > g.clusters > g.cluster",
|
|
1995
|
+
edges: ":scope > g.edgePaths > path.flowchart-link",
|
|
1996
|
+
};
|
|
1997
|
+
return [...root.querySelectorAll(selectors[kind])][index] || null;
|
|
1998
|
+
}
|
|
1999
|
+
if (sourcePath === "svg") return svg;
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function mermaidFallbackElementForBounds(svg, deck, bounds) {
|
|
2004
|
+
const ignored = new Set(["defs", "desc", "filter", "linearGradient", "marker", "metadata", "script", "style"]);
|
|
2005
|
+
const containerClasses = new Set(["root", "clusters", "edgePaths", "edgeLabels", "edgeLabel", "label", "nodes"]);
|
|
2006
|
+
const candidates = [...svg.querySelectorAll("circle, ellipse, foreignObject, g, image, line, path, polygon, polyline, rect, text, use")]
|
|
2007
|
+
.filter((candidate) => {
|
|
2008
|
+
if (candidate.closest("[data-pptx-native]")) return false;
|
|
2009
|
+
if (ignored.has(String(candidate.localName || candidate.tagName).toLowerCase())) return false;
|
|
2010
|
+
if (Array.from(candidate.classList || []).some((name) => containerClasses.has(name))) return false;
|
|
2011
|
+
const rect = candidate.getBoundingClientRect();
|
|
2012
|
+
return rect.width > 0 && rect.height > 0;
|
|
2013
|
+
});
|
|
2014
|
+
return candidates.find((candidate) => {
|
|
2015
|
+
const candidateBounds = fallbackBounds(candidate, deck);
|
|
2016
|
+
return (
|
|
2017
|
+
Math.abs(candidateBounds.x - bounds.x) <= 1 &&
|
|
2018
|
+
Math.abs(candidateBounds.y - bounds.y) <= 1 &&
|
|
2019
|
+
Math.abs(candidateBounds.width - bounds.width) <= 1 &&
|
|
2020
|
+
Math.abs(candidateBounds.height - bounds.height) <= 1
|
|
2021
|
+
);
|
|
2022
|
+
}) || null;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function markMermaidNativeElements(svg, mappedElements, pathPrefix, sourceElements) {
|
|
2026
|
+
for (const element of mappedElements) {
|
|
2027
|
+
const sourcePath = unprefixScenePath(element.path, pathPrefix);
|
|
2028
|
+
let source = sourceElements?.get(sourcePath) || mermaidElementForSourcePath(svg, sourcePath);
|
|
2029
|
+
let ownerPath = sourcePath;
|
|
2030
|
+
while (!source && sourceElements && ownerPath.includes(".")) {
|
|
2031
|
+
ownerPath = ownerPath.slice(0, ownerPath.lastIndexOf("."));
|
|
2032
|
+
source = sourceElements.get(ownerPath);
|
|
2033
|
+
}
|
|
2034
|
+
if (source && !source.hasAttribute("data-pptx-native")) {
|
|
2035
|
+
const nativeKind = element.type === "shape" ? "shape" : element.type;
|
|
2036
|
+
source.setAttribute("data-pptx-native", nativeKind);
|
|
2037
|
+
}
|
|
2038
|
+
if (!sourceElements && element.type === "connector") {
|
|
2039
|
+
const id = element.mermaid?.id || source?.getAttribute("data-id") || source?.getAttribute("id") || "";
|
|
2040
|
+
mermaidEdgeLabelElement(svg, id)?.setAttribute("data-pptx-native", "text");
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function mermaidWholeElementFallbackRequired(scene, diagnostics) {
|
|
2046
|
+
const nodes = Array.isArray(scene?.nodes) ? scene.nodes : [];
|
|
2047
|
+
const reason = nodes[0]?.reason || diagnostics.find((entry) => entry?.reason)?.reason || "";
|
|
2048
|
+
return (
|
|
2049
|
+
nodes.length === 1 &&
|
|
2050
|
+
nodes[0]?.kind === "fallback" &&
|
|
2051
|
+
nodes[0]?.sourcePath === "svg" &&
|
|
2052
|
+
(
|
|
2053
|
+
reason.startsWith("mermaid-scene-adapter-failed:") ||
|
|
2054
|
+
reason.startsWith("mermaid-scene-limit-exceeded:") ||
|
|
2055
|
+
reason === "unsupported-mermaid-svg-structure"
|
|
2056
|
+
)
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function collectMermaidObjects(element, deck, blockIndex) {
|
|
2061
|
+
const svg = element.querySelector("svg");
|
|
2062
|
+
if (!svg) {
|
|
2063
|
+
return {
|
|
2064
|
+
elements: [],
|
|
2065
|
+
fallbacks: [pptxFallback("mermaid", element, deck, "mermaid-rendered-as-artwork")],
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
const pathPrefix = `mermaid[${blockIndex}]`;
|
|
2069
|
+
const { scene, diagnostics, sourceElements } = mermaidSvgToScene(svg, {
|
|
2070
|
+
path: pathPrefix,
|
|
2071
|
+
deck,
|
|
2072
|
+
resolveColor: (value) => resolveModelColor(value, deck),
|
|
2073
|
+
includeSourceElements: true,
|
|
2074
|
+
});
|
|
2075
|
+
if (mermaidWholeElementFallbackRequired(scene, diagnostics)) {
|
|
2076
|
+
return {
|
|
2077
|
+
elements: [],
|
|
2078
|
+
fallbacks: [pptxFallback("mermaid", element, deck, "mermaid-rendered-as-artwork")],
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
const mapped = sceneToPptxElements(scene, {
|
|
2082
|
+
pathPrefix,
|
|
2083
|
+
groupPreset: "rect",
|
|
2084
|
+
zOrderBase: Number(element.dataset.pptxZOrder),
|
|
2085
|
+
});
|
|
2086
|
+
markMermaidNativeElements(svg, mapped.elements, pathPrefix, sourceElements);
|
|
2087
|
+
const fallbackNodes = new Map(
|
|
2088
|
+
scene.nodes
|
|
2089
|
+
.filter((node) => node.kind === "fallback")
|
|
2090
|
+
.map((node) => [node.sourcePath, node]),
|
|
2091
|
+
);
|
|
2092
|
+
const fallbacks = mapped.fallbacks.map((fallback) => {
|
|
2093
|
+
const sourcePath = fallback.sourcePath || unprefixScenePath(fallback.path, pathPrefix);
|
|
2094
|
+
const node = fallbackNodes.get(sourcePath);
|
|
2095
|
+
const source =
|
|
2096
|
+
sourceElements?.get(sourcePath) ||
|
|
2097
|
+
mermaidElementForSourcePath(svg, sourcePath) ||
|
|
2098
|
+
mermaidFallbackElementForBounds(svg, deck, fallback) ||
|
|
2099
|
+
svg;
|
|
2100
|
+
const sourceBounds = source.getBoundingClientRect();
|
|
2101
|
+
const padding = ["path", "line", "polyline"].includes(source.localName)
|
|
2102
|
+
? Math.max(1, (fallback.width - sourceBounds.width) / 2, (fallback.height - sourceBounds.height) / 2)
|
|
2103
|
+
: 0;
|
|
2104
|
+
const captured = pptxFallback("mermaid", source, deck, fallback.reason, {
|
|
2105
|
+
captureElement: source,
|
|
2106
|
+
includeDescendants: true,
|
|
2107
|
+
artwork: fallback.artwork,
|
|
2108
|
+
padding,
|
|
2109
|
+
});
|
|
2110
|
+
return {
|
|
2111
|
+
...captured,
|
|
2112
|
+
path: fallback.path,
|
|
2113
|
+
sourcePath,
|
|
2114
|
+
reason: fallback.reason,
|
|
2115
|
+
x: fallback.x,
|
|
2116
|
+
y: fallback.y,
|
|
2117
|
+
width: fallback.width,
|
|
2118
|
+
height: fallback.height,
|
|
2119
|
+
zOrder: fallback.zOrder,
|
|
2120
|
+
...(node?.id ? { id: node.id } : {}),
|
|
2121
|
+
...(fallback.artwork === false ? { artwork: false } : {}),
|
|
2122
|
+
};
|
|
2123
|
+
});
|
|
2124
|
+
return { elements: mapped.elements, fallbacks };
|
|
1961
2125
|
}
|
|
1962
2126
|
|
|
1963
2127
|
async function collectPptxSlide(slide, index) {
|
|
@@ -2064,10 +2228,17 @@ async function collectPptxSlide(slide, index) {
|
|
|
2064
2228
|
});
|
|
2065
2229
|
}
|
|
2066
2230
|
});
|
|
2067
|
-
deck.querySelectorAll("pre.mermaid").
|
|
2231
|
+
for (const [blockIndex, element] of [...deck.querySelectorAll("pre.mermaid")].entries()) {
|
|
2068
2232
|
const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
2233
|
+
if (covered) continue;
|
|
2234
|
+
try {
|
|
2235
|
+
const mermaid = collectMermaidObjects(element, deck, blockIndex);
|
|
2236
|
+
elements.push(...mermaid.elements);
|
|
2237
|
+
fallbacks.push(...mermaid.fallbacks);
|
|
2238
|
+
} catch (_) {
|
|
2239
|
+
addFallback("mermaid", element, "mermaid-rendered-as-artwork");
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2071
2242
|
|
|
2072
2243
|
const insideFallback = (element) =>
|
|
2073
2244
|
[...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
@@ -2079,6 +2250,7 @@ async function collectPptxSlide(slide, index) {
|
|
|
2079
2250
|
(element) =>
|
|
2080
2251
|
!insideFallback(element) &&
|
|
2081
2252
|
!element.closest(".architecture-diagram") &&
|
|
2253
|
+
!element.closest("pre.mermaid, .mermaid") &&
|
|
2082
2254
|
!element.closest("table") &&
|
|
2083
2255
|
!(element.matches("p") && element.closest("blockquote, li")),
|
|
2084
2256
|
);
|
|
@@ -2475,7 +2647,7 @@ async function renderPptxDeck(
|
|
|
2475
2647
|
}
|
|
2476
2648
|
const token = ++renderToken;
|
|
2477
2649
|
for (const slide of rendered) {
|
|
2478
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2650
|
+
await runMermaid(slide.bodyEl, slide.deck, token, false);
|
|
2479
2651
|
}
|
|
2480
2652
|
await waitForImages(stage);
|
|
2481
2653
|
await afterLayout();
|
|
@@ -2590,7 +2762,7 @@ async function renderPrintDeck(
|
|
|
2590
2762
|
}
|
|
2591
2763
|
}
|
|
2592
2764
|
for (const slide of rendered) {
|
|
2593
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2765
|
+
await runMermaid(slide.bodyEl, slide.deck, renderToken, false);
|
|
2594
2766
|
}
|
|
2595
2767
|
await waitForImages(stage);
|
|
2596
2768
|
await afterLayout();
|
|
@@ -2669,7 +2841,7 @@ async function renderCaptureSlide(
|
|
|
2669
2841
|
}
|
|
2670
2842
|
|
|
2671
2843
|
const token = ++renderToken;
|
|
2672
|
-
await runMermaid(slide.bodyEl, slide.
|
|
2844
|
+
await runMermaid(slide.bodyEl, slide.deck, token, false);
|
|
2673
2845
|
await waitForImages(stage);
|
|
2674
2846
|
await afterLayout();
|
|
2675
2847
|
|
|
@@ -2790,8 +2962,10 @@ let pptxExportAvailable = false;
|
|
|
2790
2962
|
let markdownImportAvailable = false;
|
|
2791
2963
|
let presenterViewOpen = false;
|
|
2792
2964
|
let presenterViewRequested = false;
|
|
2793
|
-
let
|
|
2794
|
-
let
|
|
2965
|
+
let exportPending = false;
|
|
2966
|
+
let exportNotificationTimer = null;
|
|
2967
|
+
let exportNotificationRemaining = 0;
|
|
2968
|
+
let exportNotificationStarted = 0;
|
|
2795
2969
|
|
|
2796
2970
|
// Derive a short overview title from a slide fragment: first heading, else first
|
|
2797
2971
|
// non-empty body line, trimmed. Mirrors the skill's title rule.
|
|
@@ -3450,93 +3624,109 @@ function updatePresenterButton(running, message = "") {
|
|
|
3450
3624
|
syncMoreControls();
|
|
3451
3625
|
}
|
|
3452
3626
|
|
|
3453
|
-
|
|
3454
|
-
if (
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3627
|
+
function pauseExportNotification() {
|
|
3628
|
+
if (exportNotificationTimer === null) return;
|
|
3629
|
+
clearTimeout(exportNotificationTimer);
|
|
3630
|
+
exportNotificationTimer = null;
|
|
3631
|
+
exportNotificationRemaining = Math.max(
|
|
3632
|
+
0,
|
|
3633
|
+
exportNotificationRemaining - (performance.now() - exportNotificationStarted),
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3462
3636
|
|
|
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;
|
|
3637
|
+
function dismissExportNotification() {
|
|
3638
|
+
const notification = document.getElementById("exportNotification");
|
|
3639
|
+
const restoreFocus = notification.contains(document.activeElement);
|
|
3640
|
+
pauseExportNotification();
|
|
3641
|
+
exportNotificationRemaining = 0;
|
|
3642
|
+
notification.hidden = true;
|
|
3643
|
+
document.getElementById("exportStatus").textContent = "";
|
|
3644
|
+
document.getElementById("exportErrorStatus").textContent = "";
|
|
3645
|
+
if (restoreFocus) {
|
|
3646
|
+
const more = document.getElementById("navMore");
|
|
3647
|
+
(more.getClientRects().length ? more : document.body).focus();
|
|
3494
3648
|
}
|
|
3495
3649
|
}
|
|
3496
3650
|
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3651
|
+
function resumeExportNotification() {
|
|
3652
|
+
const notification = document.getElementById("exportNotification");
|
|
3653
|
+
if (
|
|
3654
|
+
notification.hidden ||
|
|
3655
|
+
notification.dataset.state !== "success" ||
|
|
3656
|
+
notification.matches(":hover, :focus-within") ||
|
|
3657
|
+
exportNotificationTimer !== null
|
|
3658
|
+
) {
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
exportNotificationStarted = performance.now();
|
|
3662
|
+
exportNotificationTimer = setTimeout(dismissExportNotification, exportNotificationRemaining);
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
function showExportNotification(state, message, path = "") {
|
|
3666
|
+
pauseExportNotification();
|
|
3667
|
+
const notification = document.getElementById("exportNotification");
|
|
3668
|
+
const location = document.getElementById("exportNotificationPath");
|
|
3669
|
+
notification.dataset.state = state;
|
|
3670
|
+
document.getElementById("exportNotificationMessage").textContent = message;
|
|
3671
|
+
location.textContent = path ? `Saved to: ${path}` : "";
|
|
3672
|
+
location.hidden = !path;
|
|
3673
|
+
document.getElementById("exportNotificationClose").hidden = state === "pending";
|
|
3674
|
+
notification.hidden = false;
|
|
3675
|
+
const announcement = path ? `${message} Saved to: ${path}` : message;
|
|
3676
|
+
document.getElementById("exportStatus").textContent = state === "error" ? "" : announcement;
|
|
3677
|
+
document.getElementById("exportErrorStatus").textContent = state === "error" ? announcement : "";
|
|
3678
|
+
exportNotificationRemaining = state === "success" ? 8000 : 0;
|
|
3679
|
+
resumeExportNotification();
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
async function exportFromCanvas(format) {
|
|
3683
|
+
const isPdf = format === "pdf";
|
|
3684
|
+
if (exportPending || !(isPdf ? pdfExportAvailable : pptxExportAvailable)) return;
|
|
3685
|
+
exportPending = true;
|
|
3686
|
+
const label = isPdf ? "PDF" : "PowerPoint";
|
|
3501
3687
|
const pdfButton = document.getElementById("navExport");
|
|
3502
|
-
const
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3688
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
3689
|
+
const button = isPdf ? pdfButton : pptxButton;
|
|
3690
|
+
const idleTitle = button.title;
|
|
3691
|
+
pdfButton.disabled = true;
|
|
3692
|
+
pptxButton.disabled = true;
|
|
3693
|
+
delete button.dataset.state;
|
|
3694
|
+
button.title = `Saving ${label}.`;
|
|
3695
|
+
showExportNotification("pending", `Saving ${label}...`);
|
|
3696
|
+
syncMoreControls();
|
|
3506
3697
|
|
|
3507
3698
|
try {
|
|
3508
|
-
const response = await fetch("./export-pptx", {
|
|
3699
|
+
const response = await fetch(isPdf ? "./export" : "./export-pptx", {
|
|
3509
3700
|
method: "POST",
|
|
3510
3701
|
headers: { Accept: "application/json" },
|
|
3511
3702
|
cache: "no-store",
|
|
3512
3703
|
});
|
|
3513
|
-
const data = await response.json()
|
|
3514
|
-
if (!response.ok) {
|
|
3515
|
-
throw new Error(
|
|
3704
|
+
const data = await response.json();
|
|
3705
|
+
if (!response.ok || data?.ok !== true) {
|
|
3706
|
+
throw new Error(
|
|
3707
|
+
(typeof data?.message === "string" && data.message) ||
|
|
3708
|
+
`${label} export failed (${response.status}).`,
|
|
3709
|
+
);
|
|
3516
3710
|
}
|
|
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;
|
|
3711
|
+
if (typeof data.path !== "string" || !data.path.trim() || !data.path.split(/[\\/]/).pop()) {
|
|
3712
|
+
throw new Error(`${label} export returned an invalid save location.`);
|
|
3525
3713
|
}
|
|
3526
|
-
|
|
3714
|
+
const filename = data.path.split(/[\\/]/).pop();
|
|
3715
|
+
const fallback =
|
|
3716
|
+
!isPdf && data.fallbackCount > 0 ? ` ${data.fallbackCount} fallback item(s) preserved.` : "";
|
|
3717
|
+
const message = `${label} saved: ${filename}.${fallback}`;
|
|
3718
|
+
showExportNotification("success", message, data.path);
|
|
3527
3719
|
} 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();
|
|
3720
|
+
const message = `Could not save ${label}. ${error?.message || "Export failed."}`;
|
|
3721
|
+
console.error(`${label} export failed`, error);
|
|
3722
|
+
showExportNotification("error", message);
|
|
3536
3723
|
} finally {
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3724
|
+
exportPending = false;
|
|
3725
|
+
pdfButton.disabled = false;
|
|
3726
|
+
pptxButton.disabled = false;
|
|
3727
|
+
delete button.dataset.state;
|
|
3728
|
+
button.title = idleTitle;
|
|
3729
|
+
syncMoreControls();
|
|
3540
3730
|
}
|
|
3541
3731
|
}
|
|
3542
3732
|
|
|
@@ -3982,8 +4172,8 @@ function wireControls() {
|
|
|
3982
4172
|
bind("navPresent", openPresenterWindow, { closeMore: true });
|
|
3983
4173
|
bind("navPresenterView", openPresenterView, { closeMore: true });
|
|
3984
4174
|
bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
|
|
3985
|
-
bind("navExport",
|
|
3986
|
-
bind("navExportPptx",
|
|
4175
|
+
bind("navExport", () => exportFromCanvas("pdf"), { closeMore: true });
|
|
4176
|
+
bind("navExportPptx", () => exportFromCanvas("pptx"), { closeMore: true });
|
|
3987
4177
|
bind("navImport", toggleImportPicker, { closeMore: true });
|
|
3988
4178
|
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
3989
4179
|
bind("overviewClose", closeOverview);
|
|
@@ -3995,6 +4185,13 @@ function wireControls() {
|
|
|
3995
4185
|
bind("presenterToggleButton", togglePresenterWindow);
|
|
3996
4186
|
bind("presenterReturnButton", closePresenterView);
|
|
3997
4187
|
|
|
4188
|
+
const exportNotification = document.getElementById("exportNotification");
|
|
4189
|
+
document.getElementById("exportNotificationClose").addEventListener("click", dismissExportNotification);
|
|
4190
|
+
exportNotification.addEventListener("pointerenter", pauseExportNotification);
|
|
4191
|
+
exportNotification.addEventListener("pointerleave", resumeExportNotification);
|
|
4192
|
+
exportNotification.addEventListener("focusin", pauseExportNotification);
|
|
4193
|
+
exportNotification.addEventListener("focusout", () => queueMicrotask(resumeExportNotification));
|
|
4194
|
+
|
|
3998
4195
|
const importFilter = document.getElementById("importFilter");
|
|
3999
4196
|
if (importFilter) {
|
|
4000
4197
|
importFilter.addEventListener("input", renderImportList);
|